Compare commits

..

23 Commits

Author SHA1 Message Date
dependabot[bot] 4319460fe7 Bump the github-actions-dependencies group across 1 directory with 2 updates
Bumps the github-actions-dependencies group with 2 updates in the / directory: [docker/login-action](https://github.com/docker/login-action) and [actions/setup-go](https://github.com/actions/setup-go).


Updates `docker/login-action` from 4.2.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...af1e73f918a031802d376d3c8bbc3fe56130a9b0)

Updates `actions/setup-go` from 6.4.0 to 6.5.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...924ae3a1cded613372ab5595356fb5720e22ba16)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 01:33:42 +00:00
Owen Schwartz d4c52bbf2f Merge pull request #3396 from fosrl/dev
1.19.4-s.5
2026-07-03 16:15:18 -04:00
Owen 390c822bb4 Fix issue with overlapping site resources not sending 2026-07-03 16:13:50 -04:00
Owen 2bfc1901a6 Dont check the subnet because we dont use it 2026-07-03 14:38:01 -04:00
Owen 5da186b528 Quiet warning messages 2026-07-03 11:58:00 -04:00
Owen 600a96c13b Update rate limit to 10 2026-07-03 11:52:59 -04:00
Owen e4e0da3723 Add not exists check 2026-07-03 11:24:53 -04:00
Owen Schwartz 87f50bf0cc Merge pull request #3389 from fosrl/dev
1.19.4-s.3
2026-07-03 10:29:15 -04:00
Owen 440ebfe08e Clarify error messages 2026-07-03 10:26:13 -04:00
Owen b399d2a291 Add some retry and database confict mitigation 2026-07-03 10:23:32 -04:00
Owen Schwartz 2c66da1b19 Merge pull request #3386 from v1rusnl/main
Upgrade Traefik image to version 3.7
2026-07-03 10:18:44 -04:00
Owen 1b1fba60f1 Make sure to retry rebuilds 2026-07-03 09:02:08 -04:00
v1rusnl ab19955502 Upgrade Traefik image to version 3.7 2026-07-03 08:22:13 +02:00
v1rusnl 1db9dcec81 Update Traefik image version to v3.7 2026-07-03 08:21:12 +02:00
Owen b93d26f09f Fix reading from replicas 2026-07-02 21:46:53 -04:00
Owen fc54ad49b5 Fix trial showing 2026-07-02 21:46:44 -04:00
Owen f87e136f6b Unique subnets for exit nodes 2026-07-02 20:54:59 -04:00
Owen 1bf3d2cdd6 Add back the sync with semver 2026-07-02 12:10:20 -04:00
Owen 5fc5a3ebca Adjust spacing 2026-07-02 11:49:49 -04:00
Owen Schwartz 49c2d3163e Merge pull request #3381 from fosrl/dev
dev
2026-07-02 10:56:39 -04:00
Owen e40f325703 Add new limits to the billing page 2026-07-02 10:55:46 -04:00
Owen 8f377a4fb2 Dont run on cloud 2026-07-01 22:07:33 -04:00
Owen Schwartz 45b9e13a13 Merge pull request #3378 from fosrl/dev
1.19.4-s.1
2026-07-01 21:48:01 -04:00
41 changed files with 1099 additions and 1011 deletions
+5 -5
View File
@@ -77,7 +77,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -204,7 +204,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -264,7 +264,7 @@ jobs:
shell: bash
- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: 1.25
@@ -407,7 +407,7 @@ jobs:
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.6
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}}
traefik:
image: docker.io/traefik:v3.6
image: docker.io/traefik:v3.7
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Базово удостоверяване чрез заглавие",
"policyAuthHeaderAuthDescription": "Валидирайте собствено HTTP заглавие и стойност при всяка заявка",
"policyAuthHeaderAuthSummary": "Конфигурирано заглавие",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Име на заглавието",
"policyAuthHeaderValue": "Очаквана стойност",
"policyAuthSetPasscode": "Задайте парола",
"policyAuthSetPincode": "Задайте ПИН код",
"policyAuthSetEmailWhitelist": "Задайте списък с имейли",
@@ -1915,9 +1915,6 @@
"billingDomains": "Домейни",
"billingOrganizations": "Организации",
"billingRemoteExitNodes": "Дистанционни възли",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Няма конфигуриран лимит",
"billingEstimatedPeriod": "Очакван период на фактуриране",
"billingIncludedUsage": "Включено използване",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Колко потребители можете да използвате",
"billingDomainInfo": "Колко домейни можете да използвате",
"billingRemoteExitNodesInfo": "Колко дистанционни възли можете да използвате",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Лицензионни ключове",
"billingLicenseKeysDescription": "Управлявайте вашите абонаменти за лицензионни ключове",
"billingLicenseSubscription": "Абонамент за лиценз",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
"resourceLauncherTitle": "Стартер за Ресурси",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Преглеждайте детайлите на ресурса и ги стартирайте от едно място",
"resourceLauncherSearchPlaceholder": "Търсете във всички сайтове...",
"resourceLauncherDefaultView": "По Подразбиране",
"resourceLauncherSaveView": "Запазете Изгледа",
"resourceLauncherSaveToCurrentView": "Запазете в Текущ Изглед",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Нулирайте Изгледа",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Запазете като Нов Изглед",
"resourceLauncherSaveAsNewViewDescription": "Дайте име на този изглед, за да запазите текущите си филтри и оформление.",
"resourceLauncherSaveForEveryone": "Запазете за Всеки",
"resourceLauncherSaveForEveryoneDescription": "Споделете този изглед с всички членове на организацията. Когато е изключено, изгледът е видим само за вас.",
"resourceLauncherMakePersonal": "Направи Личен",
"resourceLauncherFilter": "Филтър",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Сортиране",
"resourceLauncherSortAscending": "Сортиране възходящо",
"resourceLauncherSortDescending": "Сортиране низходящо",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Групирай По",
"resourceLauncherGroupBySite": "Сайт",
"resourceLauncherGroupByLabel": "Етикет",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Оформление",
"resourceLauncherLayoutGrid": "Мрежа",
"resourceLauncherLayoutList": "Списък",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Показване на Тагове на Сайт",
"resourceLauncherShowRecents": "Показване на Последни",
"resourceLauncherDeleteView": "Изтриване на Изглед",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Вижте като Админ",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Вижте детайлите за този ресурс.",
"resourceLauncherUnlabeled": "Без Етикет",
"resourceLauncherNoSite": "Няма Сайт",
"resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Няма Намерени Ресурси",
"resourceLauncherEmptyStateNoResultsDescription": "Никакви ресурси не съвпадат с текущото ви търсене или филтри. Опитайте да ги коригирате, за да намерите това, което търсите.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Никакви ресурси не съвпадат с \"{query}\". Опитайте да коригирате търсенето или да изтриете филтри, за да видите всички ресурси.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Копирано в клипборда",
"resourceLauncherCopiedAccessDescription": "Достъпът до ресурса е копиран на вашия клипборд.",
"resourceLauncherViewNamePlaceholder": "Име на Изгледа",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Základní Ověření Záhlaví",
"policyAuthHeaderAuthDescription": "Ověřit vlastní HTTP hlavičku názvu a hodnoty při každém požadavku",
"policyAuthHeaderAuthSummary": "Nastaveno hlavička",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Název hlavičky",
"policyAuthHeaderValue": "Očekávaná hodnota",
"policyAuthSetPasscode": "Nastavit přístupový kód",
"policyAuthSetPincode": "Nastavit PIN kód",
"policyAuthSetEmailWhitelist": "Nastavit e-mailový whitelist",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domény",
"billingOrganizations": "Tělo",
"billingRemoteExitNodes": "Vzdálené uzly",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Žádný limit nenastaven",
"billingEstimatedPeriod": "Odhadované období fakturace",
"billingIncludedUsage": "Zahrnuto využití",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Kolik uživatelů můžete použít",
"billingDomainInfo": "Kolik domén můžete použít",
"billingRemoteExitNodesInfo": "Kolik vzdálených uzlů můžete použít",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Licenční klíče",
"billingLicenseKeysDescription": "Spravovat předplatné licenčního klíče",
"billingLicenseSubscription": "Předplatné licence",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Zdroj je zakázán",
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
"resourceLauncherTitle": "Spouštěč zdrojů",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Podívejte se na podrobnosti o zdrojích a spusťte je z jednoho místa",
"resourceLauncherSearchPlaceholder": "Hledat všechny lokality...",
"resourceLauncherDefaultView": "Výchozí",
"resourceLauncherSaveView": "Uložit pohled",
"resourceLauncherSaveToCurrentView": "Uložit do aktuálního pohledu",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Obnovit pohled",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Uložit jako nový pohled",
"resourceLauncherSaveAsNewViewDescription": "Uložte tento pohled k uloženému filtrování a rozvržení.",
"resourceLauncherSaveForEveryone": "Uložit pro všechny",
"resourceLauncherSaveForEveryoneDescription": "Sdílejte tento pohled se všemi členy organizace. Pokud není zaškrtnuto, pohled je viditelný pouze vám.",
"resourceLauncherMakePersonal": "Udělat osobní",
"resourceLauncherFilter": "Filtr",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Řadit",
"resourceLauncherSortAscending": "Řadit vzestupně",
"resourceLauncherSortDescending": "Řadit sestupně",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Seskupit podle",
"resourceLauncherGroupBySite": "Lokalita",
"resourceLauncherGroupByLabel": "Název",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Rozvržení",
"resourceLauncherLayoutGrid": "Mřížka",
"resourceLauncherLayoutList": "Seznam",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Zobrazit značky lokality",
"resourceLauncherShowRecents": "Zobrazit nedávné",
"resourceLauncherDeleteView": "Smazat pohled",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Zobrazit jako administrátor",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Podívejte se na podrobnosti o tomto zdroji.",
"resourceLauncherUnlabeled": "Bez nálepky",
"resourceLauncherNoSite": "Žádná lokalita",
"resourceLauncherNoResourcesInGroup": "V této skupině nejsou žádné zdroje",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Nebyl nalezen žádný zdroj",
"resourceLauncherEmptyStateNoResultsDescription": "Žádný zdroj neodpovídá vašemu aktuálnímu vyhledávání nebo filtrům. Zkuste je upravit, abyste našli to, co hledáte.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Žádné zdroje neodpovídají \"{query}\". Zkuste upravit vyhledávání nebo vymazat filtry, abyste viděli všechny zdroje.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Zkopírováno do schránky",
"resourceLauncherCopiedAccessDescription": "Přístup ke zdroji byl zkopírován do vaší schránky.",
"resourceLauncherViewNamePlaceholder": "Název pohledu",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Grunnleggende Header Autentificering",
"policyAuthHeaderAuthDescription": "Bekræft et tilpasset HTTP-headernavn og værdi ved hver forespørgsel",
"policyAuthHeaderAuthSummary": "Header konfigureret",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Headernavn",
"policyAuthHeaderValue": "Forventet værdi",
"policyAuthSetPasscode": "Angiv adgangskode",
"policyAuthSetPincode": "Sett PIN-kode",
"policyAuthSetEmailWhitelist": "Angiv e-mail hviteliste",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domæner",
"billingOrganizations": "Orger",
"billingRemoteExitNodes": "Eksterne noder",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Ingen grænse konfigureret",
"billingEstimatedPeriod": "Estimert faktureringsperiode",
"billingIncludedUsage": "Inklusive Brug",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Hvor mange brugere du kan bruge",
"billingDomainInfo": "Hvor mange domæner du kan bruge",
"billingRemoteExitNodesInfo": "Hvor mange fjernnoder du kan bruge",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Licensnøgler",
"billingLicenseKeysDescription": "Administrer dine licensnøgleabonnementer",
"billingLicenseSubscription": "Licens abonnement",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ressource deaktiveret",
"memberPortalShowingResources": "Viser {start}-{end} af {total} ressourcer",
"resourceLauncherTitle": "Ressource Starter",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Se ressource detaljer og start dem fra ét sted",
"resourceLauncherSearchPlaceholder": "Søg i alle sites...",
"resourceLauncherDefaultView": "Standard",
"resourceLauncherSaveView": "Gem Visning",
"resourceLauncherSaveToCurrentView": "Gem til nuværende visning",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Nulstil Visning",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Gem som Ny Visning",
"resourceLauncherSaveAsNewViewDescription": "Giv denne visning et navn for at gemme dine nuværende filtre og layout.",
"resourceLauncherSaveForEveryone": "Gem for Alle",
"resourceLauncherSaveForEveryoneDescription": "Del denne visning med alle organisationsmedlemmer. Når den er ikke markeret, er visningen kun synlig for dig.",
"resourceLauncherMakePersonal": "Gør Personlig",
"resourceLauncherFilter": "Filter",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sortér",
"resourceLauncherSortAscending": "Sortér stigende",
"resourceLauncherSortDescending": "Sortér faldende",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Gruppér Efter",
"resourceLauncherGroupBySite": "Websted",
"resourceLauncherGroupByLabel": "Etikett",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Layout",
"resourceLauncherLayoutGrid": "Gitter",
"resourceLauncherLayoutList": "Liste",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Vis Site Tags",
"resourceLauncherShowRecents": "Vis Seneste",
"resourceLauncherDeleteView": "Slet Visning",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Vis som Admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Se detaljer for denne ressource.",
"resourceLauncherUnlabeled": "Uden Etiket",
"resourceLauncherNoSite": "Ingen Site",
"resourceLauncherNoResourcesInGroup": "Ingen ressourcer i denne gruppe",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Ingen ressourcer fundet",
"resourceLauncherEmptyStateNoResultsDescription": "Ingen ressourcer matcher din nuværende søgning eller filtre. Prøv at justere dem for at finde det, du leder efter.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Ingen ressourcer matcher \"{query}\". Prøv at justere din søgning eller rydde filtre for at se alle ressourcer.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Kopieret til udklipsholderen",
"resourceLauncherCopiedAccessDescription": "Ressourcetilgangen er kopieret til din udklipsholder.",
"resourceLauncherViewNamePlaceholder": "Vis navn",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Grundlegende Header-Authentifizierung",
"policyAuthHeaderAuthDescription": "Überprüfen Sie einen benutzerdefinierten HTTP-Headernamen und -wert bei jeder Anfrage",
"policyAuthHeaderAuthSummary": "Header konfiguriert",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Header-Name",
"policyAuthHeaderValue": "Erwarteter Wert",
"policyAuthSetPasscode": "Passcode setzen",
"policyAuthSetPincode": "PIN-Code festlegen",
"policyAuthSetEmailWhitelist": "E-Mail-Whitelist festlegen",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domänen",
"billingOrganizations": "Orden",
"billingRemoteExitNodes": "Entfernte Knoten",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Kein Limit konfiguriert",
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
"billingIncludedUsage": "Inklusive Nutzung",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Wie viele Benutzer Sie verwenden können",
"billingDomainInfo": "Wie viele Domains Sie verwenden können",
"billingRemoteExitNodesInfo": "Wie viele entfernte Knoten Sie verwenden können",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Lizenzschlüssel",
"billingLicenseKeysDescription": "Verwalten Sie Ihre Lizenzschlüssel Abonnements",
"billingLicenseSubscription": "Lizenzabonnement",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ressource deaktiviert",
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
"resourceLauncherTitle": "Ressourcenstarter",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Sehen Sie sich Ressourcendetails an und starten Sie sie von einem Ort aus",
"resourceLauncherSearchPlaceholder": "Alle Standorte durchsuchen...",
"resourceLauncherDefaultView": "Standard",
"resourceLauncherSaveView": "Ansicht speichern",
"resourceLauncherSaveToCurrentView": "In aktueller Ansicht speichern",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Ansicht zurücksetzen",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Als neue Ansicht speichern",
"resourceLauncherSaveAsNewViewDescription": "Geben Sie dieser Ansicht einen Namen, um Ihre aktuellen Filter und das Layout zu speichern.",
"resourceLauncherSaveForEveryone": "Für alle speichern",
"resourceLauncherSaveForEveryoneDescription": "Teilen Sie diese Ansicht mit allen Organisationsmitgliedern. Wenn nicht aktiviert, ist die Ansicht nur für Sie sichtbar.",
"resourceLauncherMakePersonal": "Persönlich machen",
"resourceLauncherFilter": "Filter",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sortieren",
"resourceLauncherSortAscending": "Aufsteigend sortieren",
"resourceLauncherSortDescending": "Absteigend sortieren",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Gruppieren nach",
"resourceLauncherGroupBySite": "Standort",
"resourceLauncherGroupByLabel": "Etikett",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Layout",
"resourceLauncherLayoutGrid": "Raster",
"resourceLauncherLayoutList": "Liste",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Standort-Tags anzeigen",
"resourceLauncherShowRecents": "Kürzlich anzeigen",
"resourceLauncherDeleteView": "Ansicht löschen",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Ansicht als Administrator anzeigen",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Anzeigen von Details zu dieser Ressource.",
"resourceLauncherUnlabeled": "Nicht etikettiert",
"resourceLauncherNoSite": "Kein Standort",
"resourceLauncherNoResourcesInGroup": "Keine Ressourcen in dieser Gruppe",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Keine Ressourcen gefunden",
"resourceLauncherEmptyStateNoResultsDescription": "Keine Ressourcen entsprechen Ihrer aktuellen Suche oder den Filtern. Versuchen Sie, diese anzupassen, um zu finden, wonach Sie suchen.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Keine Ressourcen entsprechen \"{query}\". Versuchen Sie, Ihre Suche anzupassen oder die Filter zu löschen, um alle Ressourcen anzuzeigen.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "In die Zwischenablage kopiert",
"resourceLauncherCopiedAccessDescription": "Der Ressourcenzugriff wurde in Ihre Zwischenablage kopiert.",
"resourceLauncherViewNamePlaceholder": "Ansichtsname",
+6
View File
@@ -1915,6 +1915,9 @@
"billingDomains": "Domains",
"billingOrganizations": "Orgs",
"billingRemoteExitNodes": "Remote Nodes",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "No limit configured",
"billingEstimatedPeriod": "Estimated Billing Period",
"billingIncludedUsage": "Included Usage",
@@ -1943,6 +1946,9 @@
"billingUsersInfo": "How many users you can use",
"billingDomainInfo": "How many domains you can use",
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "License Keys",
"billingLicenseKeysDescription": "Manage your license key subscriptions",
"billingLicenseSubscription": "License Subscription",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Autenticación Básica del Encabezado",
"policyAuthHeaderAuthDescription": "Valida un nombre y valor de encabezado HTTP personalizado en cada petición",
"policyAuthHeaderAuthSummary": "Encabezado configurado",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Nombre del encabezado",
"policyAuthHeaderValue": "Valor esperado",
"policyAuthSetPasscode": "Establecer Código de Acceso",
"policyAuthSetPincode": "Establecer Código PIN",
"policyAuthSetEmailWhitelist": "Establecer Lista Blanca de Correo",
@@ -1915,9 +1915,6 @@
"billingDomains": "Dominios",
"billingOrganizations": "Orgánico",
"billingRemoteExitNodes": "Nodos remotos",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "No se ha configurado ningún límite",
"billingEstimatedPeriod": "Período de facturación estimado",
"billingIncludedUsage": "Uso incluido",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Cuántos usuarios puedes usar",
"billingDomainInfo": "Cuántos dominios puedes usar",
"billingRemoteExitNodesInfo": "Cuántos nodos remotos puedes usar",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Claves de licencia",
"billingLicenseKeysDescription": "Administrar las suscripciones de su clave de licencia",
"billingLicenseSubscription": "Suscripción de licencia",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Recurso Deshabilitado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
"resourceLauncherTitle": "Lanzador de Recursos",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Ve los detalles de los recursos y lánzalos desde un solo lugar",
"resourceLauncherSearchPlaceholder": "Buscar en todos los sitios...",
"resourceLauncherDefaultView": "Predeterminado",
"resourceLauncherSaveView": "Guardar Vista",
"resourceLauncherSaveToCurrentView": "Guardar en la Vista Actual",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Restablecer Vista",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Guardar como Nueva Vista",
"resourceLauncherSaveAsNewViewDescription": "Ponle un nombre a esta vista para guardar tus filtros y diseño actuales.",
"resourceLauncherSaveForEveryone": "Guardar para Todos",
"resourceLauncherSaveForEveryoneDescription": "Comparte esta vista con todos los miembros de la organización. Si está desmarcado, la vista solo es visible para ti.",
"resourceLauncherMakePersonal": "Hacer Personal",
"resourceLauncherFilter": "Filtro",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Ordenar",
"resourceLauncherSortAscending": "Ordenar Ascendente",
"resourceLauncherSortDescending": "Ordenar Descendente",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Agrupar Por",
"resourceLauncherGroupBySite": "Sitio",
"resourceLauncherGroupByLabel": "Etiqueta",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Disposición",
"resourceLauncherLayoutGrid": "Cuadrícula",
"resourceLauncherLayoutList": "Lista",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Mostrar Etiquetas del Sitio",
"resourceLauncherShowRecents": "Mostrar Recientes",
"resourceLauncherDeleteView": "Eliminar Vista",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Ver como Administrador",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Ver detalles de este recurso.",
"resourceLauncherUnlabeled": "Sin Etiqueta",
"resourceLauncherNoSite": "Sin Sitio",
"resourceLauncherNoResourcesInGroup": "No hay recursos en este grupo",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "No se Encontraron Recursos",
"resourceLauncherEmptyStateNoResultsDescription": "No hay recursos que coincidan con tu búsqueda o filtros actuales. Intenta ajustarlos para encontrar lo que buscas.",
"resourceLauncherEmptyStateNoResultsWithQuery": "No hay recursos que coincidan con \"{query}\". Intenta ajustar tu búsqueda o borrar filtros para ver todos los recursos.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Copiado al portapapeles",
"resourceLauncherCopiedAccessDescription": "El acceso al recurso ha sido copiado a tu portapapeles.",
"resourceLauncherViewNamePlaceholder": "Nombre de la Vista",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Authentification de l'en-tête de base",
"policyAuthHeaderAuthDescription": "Validez un nom et une valeur d'en-tête HTTP personnalisé à chaque requête",
"policyAuthHeaderAuthSummary": "En-tête configuré",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Nom de l'en-tête",
"policyAuthHeaderValue": "Valeur attendue",
"policyAuthSetPasscode": "Définir le code confidentiel",
"policyAuthSetPincode": "Définir le code PIN",
"policyAuthSetEmailWhitelist": "Définir la liste blanche des e-mails",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domaines",
"billingOrganizations": "Organes",
"billingRemoteExitNodes": "Nœuds distants",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Aucune limite configurée",
"billingEstimatedPeriod": "Période de facturation estimée",
"billingIncludedUsage": "Utilisation incluse",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Combien d'utilisateurs vous pouvez utiliser",
"billingDomainInfo": "Combien de domaines vous pouvez utiliser",
"billingRemoteExitNodesInfo": "Combien de nœuds distants vous pouvez utiliser",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Clés de licence",
"billingLicenseKeysDescription": "Gérer vos abonnements à la clé de licence",
"billingLicenseSubscription": "Abonnement à la licence",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ressource désactivée",
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
"resourceLauncherTitle": "Lanceur de ressources",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Afficher les détails des ressources et les lancer depuis un seul endroit",
"resourceLauncherSearchPlaceholder": "Rechercher sur tous les sites...",
"resourceLauncherDefaultView": "Par défaut",
"resourceLauncherSaveView": "Enregistrer la vue",
"resourceLauncherSaveToCurrentView": "Enregistrer dans la vue actuelle",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Réinitialiser la vue",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Enregistrer comme nouvelle vue",
"resourceLauncherSaveAsNewViewDescription": "Donnez un nom à cette vue pour enregistrer vos filtres et mise en page actuels.",
"resourceLauncherSaveForEveryone": "Enregistrer pour tout le monde",
"resourceLauncherSaveForEveryoneDescription": "Partagez cette vue avec tous les membres de l'organisation. Lorsque décochée, la vue est visible uniquement par vous.",
"resourceLauncherMakePersonal": "Rendre personnel",
"resourceLauncherFilter": "Filtrer",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Trier",
"resourceLauncherSortAscending": "Trier par ordre croissant",
"resourceLauncherSortDescending": "Trier par ordre décroissant",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Grouper par",
"resourceLauncherGroupBySite": "Nœud",
"resourceLauncherGroupByLabel": "Étiquette",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Mise en page",
"resourceLauncherLayoutGrid": "Grille",
"resourceLauncherLayoutList": "Liste",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Afficher les tags de site",
"resourceLauncherShowRecents": "Afficher les récents",
"resourceLauncherDeleteView": "Supprimer la vue",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Voir en tant qu'admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Afficher les détails pour cette ressource.",
"resourceLauncherUnlabeled": "Non étiqueté",
"resourceLauncherNoSite": "Aucun nœud",
"resourceLauncherNoResourcesInGroup": "Aucune ressource dans ce groupe",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Aucune ressource trouvée",
"resourceLauncherEmptyStateNoResultsDescription": "Aucune ressource ne correspond à votre recherche ou filtre actuel. Essayez de les ajuster pour trouver ce que vous cherchez.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Aucune ressource ne correspond à \"{query}\". Essayez d'ajuster votre recherche ou de supprimer les filtres pour voir toutes les ressources.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Copié dans le presse-papiers",
"resourceLauncherCopiedAccessDescription": "L'accès à la ressource a été copié dans votre presse-papiers.",
"resourceLauncherViewNamePlaceholder": "Nom de la vue",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Autenticazione Header Base",
"policyAuthHeaderAuthDescription": "Convalida un nome e un valore di intestazione HTTP personalizzato su ogni richiesta",
"policyAuthHeaderAuthSummary": "Intestazione configurata",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Nome dell'intestazione",
"policyAuthHeaderValue": "Valore atteso",
"policyAuthSetPasscode": "Imposta Codice di Accesso",
"policyAuthSetPincode": "Imposta Codice PIN",
"policyAuthSetEmailWhitelist": "Imposta Lista Autorizzazioni Email",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domini",
"billingOrganizations": "Organi",
"billingRemoteExitNodes": "Nodi Remoti",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Nessun limite configurato",
"billingEstimatedPeriod": "Periodo di Fatturazione Stimato",
"billingIncludedUsage": "Utilizzo Incluso",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Quanti utenti puoi usare",
"billingDomainInfo": "Quanti domini puoi usare",
"billingRemoteExitNodesInfo": "Quanti nodi remoti puoi usare",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Chiavi di Licenza",
"billingLicenseKeysDescription": "Gestisci le sottoscrizioni alla chiave di licenza",
"billingLicenseSubscription": "Abbonamento Licenza",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Risorsa Disabilitata",
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
"resourceLauncherTitle": "Lanscia Risorse",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Visualizza i dettagli delle risorse e lanciale da un solo posto",
"resourceLauncherSearchPlaceholder": "Cerca tutti i siti...",
"resourceLauncherDefaultView": "Predefinito",
"resourceLauncherSaveView": "Salva Visualizzazione",
"resourceLauncherSaveToCurrentView": "Salva alla Visualizzazione Corrente",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Reimposta Visualizzazione",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Salva come Nuova Visualizzazione",
"resourceLauncherSaveAsNewViewDescription": "Dai un nome a questa visualizzazione per salvare i tuoi filtri e layout attuali.",
"resourceLauncherSaveForEveryone": "Salva per Tutti",
"resourceLauncherSaveForEveryoneDescription": "Condividi questa visualizzazione con tutti i membri dell'organizzazione. Quando non è selezionata, la visualizzazione è visibile solo a te.",
"resourceLauncherMakePersonal": "Rendi Personale",
"resourceLauncherFilter": "Filtro",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Ordina",
"resourceLauncherSortAscending": "Ordina in ordine crescente",
"resourceLauncherSortDescending": "Ordina in ordine decrescente",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Raggruppa per",
"resourceLauncherGroupBySite": "Sito",
"resourceLauncherGroupByLabel": "Etichetta",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Layout",
"resourceLauncherLayoutGrid": "Griglia",
"resourceLauncherLayoutList": "Lista",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Mostra Tag di Sito",
"resourceLauncherShowRecents": "Mostra Recenti",
"resourceLauncherDeleteView": "Elimina Visualizzazione",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Visualizza come Admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Visualizza i dettagli per questa risorsa.",
"resourceLauncherUnlabeled": "Non Etichettato",
"resourceLauncherNoSite": "Nessun Sito",
"resourceLauncherNoResourcesInGroup": "Nessuna risorsa in questo gruppo",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Nessuna risorsa trovata",
"resourceLauncherEmptyStateNoResultsDescription": "Nessuna risorsa corrisponde alla tua ricerca o ai tuoi filtri attuali. Prova a modificarli per trovare ciò che stai cercando.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Nessuna risorsa corrisponde a \"{query}\". Prova a modificare la tua ricerca o a cancellare i filtri per vedere tutte le risorse.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Copiato negli appunti",
"resourceLauncherCopiedAccessDescription": "L'accesso alla risorsa è stato copiato nei tuoi appunti.",
"resourceLauncherViewNamePlaceholder": "Nome Visualizzazione",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "기본 헤더 인증",
"policyAuthHeaderAuthDescription": "각 요청에서 맞춤 HTTP 헤더 이름 및 값을 검증",
"policyAuthHeaderAuthSummary": "헤더 구성됨",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "헤더 이름",
"policyAuthHeaderValue": "예상 값",
"policyAuthSetPasscode": "패스코드 설정",
"policyAuthSetPincode": "PIN 코드 설정",
"policyAuthSetEmailWhitelist": "이메일 화이트리스트 설정",
@@ -1915,9 +1915,6 @@
"billingDomains": "도메인",
"billingOrganizations": "조직",
"billingRemoteExitNodes": "원격 노드",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "구성된 한도가 없습니다.",
"billingEstimatedPeriod": "예상 청구 기간",
"billingIncludedUsage": "포함 사용량",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "사용할 수 있는 사용자 수",
"billingDomainInfo": "사용할 수 있는 도메인 수",
"billingRemoteExitNodesInfo": "사용할 수 있는 원격 노드 수",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "라이센스 키",
"billingLicenseKeysDescription": "라이센스 키 구독을 관리하세요",
"billingLicenseSubscription": "라이센스 구독",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "리소스 비활성화됨",
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
"resourceLauncherTitle": "리소스 런처",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "리소스 세부 정보를 보고 한 곳에서 실행하세요",
"resourceLauncherSearchPlaceholder": "모든 사이트 검색...",
"resourceLauncherDefaultView": "기본값",
"resourceLauncherSaveView": "보기를 저장",
"resourceLauncherSaveToCurrentView": "현재 보기로 저장",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "보기를 재설정",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "새 보기로 저장",
"resourceLauncherSaveAsNewViewDescription": "현재 필터와 레이아웃을 저장할 이름을 입력하세요.",
"resourceLauncherSaveForEveryone": "모두에게 저장",
"resourceLauncherSaveForEveryoneDescription": "이 보기를 모든 조직 구성원과 공유합니다. 체크 해제하면 해당 뷰는 사용자에게만 표시됩니다.",
"resourceLauncherMakePersonal": "개인적으로 만들기",
"resourceLauncherFilter": "필터",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "정렬",
"resourceLauncherSortAscending": "오름차순 정렬",
"resourceLauncherSortDescending": "내림차순 정렬",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "그룹화 기준",
"resourceLauncherGroupBySite": "사이트",
"resourceLauncherGroupByLabel": "레이블",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "레이아웃",
"resourceLauncherLayoutGrid": "그리드",
"resourceLauncherLayoutList": "목록",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "사이트 태그 표시",
"resourceLauncherShowRecents": "최근 항목 표시",
"resourceLauncherDeleteView": "보기 삭제",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "관리자로 보기",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "이 리소스의 세부정보를 봅니다.",
"resourceLauncherUnlabeled": "레이블 없음",
"resourceLauncherNoSite": "사이트 없음",
"resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "리소스를 찾을 수 없음",
"resourceLauncherEmptyStateNoResultsDescription": "현재 검색이나 필터에 맞는 리소스가 없습니다. 필터를 조정하여 찾으려는 항목을 확인해보세요.",
"resourceLauncherEmptyStateNoResultsWithQuery": "\"{query}\"와 일치하는 리소스가 없습니다. 검색을 조정하거나 필터를 지워서 모든 리소스를 확인해보세요.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "클립보드에 복사됨",
"resourceLauncherCopiedAccessDescription": "리소스 액세스가 클립보드에 복사되었습니다.",
"resourceLauncherViewNamePlaceholder": "보기 이름",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Grunnleggende Header Autentisering",
"policyAuthHeaderAuthDescription": "Bekreft et tilpasset HTTP-headernavn og verdi ved hver forespørsel",
"policyAuthHeaderAuthSummary": "Header konfigurert",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Headernavn",
"policyAuthHeaderValue": "Forventet verdi",
"policyAuthSetPasscode": "Angi passordkode",
"policyAuthSetPincode": "Sett PIN-kode",
"policyAuthSetEmailWhitelist": "Angi e-post hviteliste",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domener",
"billingOrganizations": "Orger",
"billingRemoteExitNodes": "Eksterne Noder",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Ingen grense konfigurert",
"billingEstimatedPeriod": "Estimert faktureringsperiode",
"billingIncludedUsage": "Inkludert Bruk",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Hvor mange brukere du kan bruke",
"billingDomainInfo": "Hvor mange domener du kan bruke",
"billingRemoteExitNodesInfo": "Hvor mange fjernnoder du kan bruke",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Lisensnøkler",
"billingLicenseKeysDescription": "Administrer dine lisensnøkkelabonnementer",
"billingLicenseSubscription": "Lisens abonnement",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ressurs deaktivert",
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
"resourceLauncherTitle": "Ressurslansering",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Vis ressursdetaljer og start dem fra ett sted",
"resourceLauncherSearchPlaceholder": "Søk i alle områder...",
"resourceLauncherDefaultView": "Standard",
"resourceLauncherSaveView": "Lagre visning",
"resourceLauncherSaveToCurrentView": "Lagre til nåværende visning",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Tilbakestill visning",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Lagre som ny visning",
"resourceLauncherSaveAsNewViewDescription": "Gi denne visningen et navn for å lagre dine nåværende filtre og oppsett.",
"resourceLauncherSaveForEveryone": "Lagre for alle",
"resourceLauncherSaveForEveryoneDescription": "Del denne visningen med alle organisasjonsmedlemmer. Når avkrysset, er visningen synlig bare for deg.",
"resourceLauncherMakePersonal": "Gjør personlig",
"resourceLauncherFilter": "Filter",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sorter",
"resourceLauncherSortAscending": "Sorter stigende",
"resourceLauncherSortDescending": "Sorter synkende",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Grupper etter",
"resourceLauncherGroupBySite": "Område",
"resourceLauncherGroupByLabel": "Etikett",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Oppsett",
"resourceLauncherLayoutGrid": "Rutenett",
"resourceLauncherLayoutList": "Liste",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Vis områdestikkord",
"resourceLauncherShowRecents": "Vis nylige",
"resourceLauncherDeleteView": "Slett visning",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Vis som administrator",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Vis detaljer for denne ressursen.",
"resourceLauncherUnlabeled": "Umerket",
"resourceLauncherNoSite": "Ingen område",
"resourceLauncherNoResourcesInGroup": "Ingen ressurser i denne gruppen",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Ingen ressurser funnet",
"resourceLauncherEmptyStateNoResultsDescription": "Ingen ressurser matcher dine nåværende søk eller filtre. Prøv å justere dem for å finne det du leter etter.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Ingen ressurser samsvarer med \"{query}\". Prøv å justere søket eller fjern filtrene for å se alle ressursene.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Kopiert til utklippstavlen",
"resourceLauncherCopiedAccessDescription": "Ressurstilgang er kopiert til utklippstavlen din.",
"resourceLauncherViewNamePlaceholder": "Visningsnavn",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Basic Header Authenticatie",
"policyAuthHeaderAuthDescription": "Valideer een aangepaste HTTP-headernaam en waarde bij elk verzoek",
"policyAuthHeaderAuthSummary": "Header geconfigureerd",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Header naam",
"policyAuthHeaderValue": "Verwachte waarde",
"policyAuthSetPasscode": "Stel toegangscode in",
"policyAuthSetPincode": "Stel Pincode in",
"policyAuthSetEmailWhitelist": "Stel E-mail Whitelist in",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domeinen",
"billingOrganizations": "Ordenen",
"billingRemoteExitNodes": "Externe knooppunten",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Geen limiet ingesteld",
"billingEstimatedPeriod": "Geschatte Facturatie Periode",
"billingIncludedUsage": "Opgenomen Gebruik",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Hoeveel gebruikers je kan gebruiken",
"billingDomainInfo": "Hoeveel domeinen je kunt gebruiken",
"billingRemoteExitNodesInfo": "Hoeveel externe nodes je kunt gebruiken",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Licentie Sleutels",
"billingLicenseKeysDescription": "Beheer uw licentiesleutelabonnementen",
"billingLicenseSubscription": "Licentie abonnement",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
"resourceLauncherTitle": "Bron Launcher",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Bekijk brongegevens en start ze vanaf één plek",
"resourceLauncherSearchPlaceholder": "Zoek alle sites...",
"resourceLauncherDefaultView": "Standaard",
"resourceLauncherSaveView": "Weergave Opslaan",
"resourceLauncherSaveToCurrentView": "Opslaan naar huidige weergave",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Weergave Herstellen",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Opslaan als Nieuwe Weergave",
"resourceLauncherSaveAsNewViewDescription": "Geef deze weergave een naam om je huidige filters en indeling op te slaan.",
"resourceLauncherSaveForEveryone": "Opslaan voor Iedereen",
"resourceLauncherSaveForEveryoneDescription": "Deel deze weergave met alle organisatieleden. Als dit niet is aangevinkt, is de weergave alleen zichtbaar voor jou.",
"resourceLauncherMakePersonal": "Persoonlijk Maken",
"resourceLauncherFilter": "Filter",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sorteren",
"resourceLauncherSortAscending": "Oplopend sorteren",
"resourceLauncherSortDescending": "Aflopend sorteren",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Groep Op",
"resourceLauncherGroupBySite": "Site",
"resourceLauncherGroupByLabel": "Label",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Lay-out",
"resourceLauncherLayoutGrid": "Raster",
"resourceLauncherLayoutList": "Lijst",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Site Tags Weergeven",
"resourceLauncherShowRecents": "Recente Weergeven",
"resourceLauncherDeleteView": "Weergave Verwijderen",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Bekijk als Admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Bekijk details voor deze bron.",
"resourceLauncherUnlabeled": "Geen label",
"resourceLauncherNoSite": "Geen Site",
"resourceLauncherNoResourcesInGroup": "Geen bronnen in deze groep",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Geen Bronnen Gevonden",
"resourceLauncherEmptyStateNoResultsDescription": "Geen bronnen komen overeen met je huidige zoekopdracht of filters. Probeer ze aan te passen om te vinden wat je zoekt.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Geen bronnen komen overeen met \"{query}\". Probeer je zoekopdracht aan te passen of filters te wissen om alle bronnen te zien.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Gekopieerd naar klembord",
"resourceLauncherCopiedAccessDescription": "Toegang tot bron is gekopieerd naar je klembord.",
"resourceLauncherViewNamePlaceholder": "Weergavenaam",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Podstawowe Uwierzytelnianie Nagłówka",
"policyAuthHeaderAuthDescription": "Walidacja niestandardowej nazwy i wartości nagłówka HTTP przy każdym żądaniu",
"policyAuthHeaderAuthSummary": "Skonfigurowany nagłówek",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Nazwa nagłówka",
"policyAuthHeaderValue": "Oczekiwana wartość",
"policyAuthSetPasscode": "Ustaw hasło dostępu",
"policyAuthSetPincode": "Ustaw kod PIN",
"policyAuthSetEmailWhitelist": "Ustaw białą listę e-mail",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domeny",
"billingOrganizations": "O masie całkowitej pojazdu przekraczającej 5 ton, ale nieprzekraczającej 5 ton",
"billingRemoteExitNodes": "Zdalne węzły",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Nie skonfigurowano limitu",
"billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy",
"billingIncludedUsage": "Zawarte użycie",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Ile użytkowników możesz użyć",
"billingDomainInfo": "Ile domen możesz użyć",
"billingRemoteExitNodesInfo": "Ile zdalnych węzłów możesz użyć",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Klucze licencyjne",
"billingLicenseKeysDescription": "Zarządzaj subskrypcjami kluczy licencyjnych",
"billingLicenseSubscription": "Subskrypcja licencji",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Zasób wyłączony",
"memberPortalShowingResources": "Wyświetlanie zasobów od {start} do {end} z {total}",
"resourceLauncherTitle": "Uruchamiacz Zasobów",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Przeglądaj szczegóły zasobów i uruchamiaj je z jednego miejsca",
"resourceLauncherSearchPlaceholder": "Szukaj we wszystkich stronach...",
"resourceLauncherDefaultView": "Domyślny",
"resourceLauncherSaveView": "Zapisz Widok",
"resourceLauncherSaveToCurrentView": "Zapisz do bieżącego widoku",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Resetuj Widok",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Zapisz jako Nowy Widok",
"resourceLauncherSaveAsNewViewDescription": "Nadaj nazwę temu widokowi, aby zapisać swoje bieżące filtry i układ.",
"resourceLauncherSaveForEveryone": "Zapisz dla wszystkich",
"resourceLauncherSaveForEveryoneDescription": "Udostępnij ten widok wszystkim członkom organizacji. Gdy jest niezaznaczone, widok jest widoczny tylko dla Ciebie.",
"resourceLauncherMakePersonal": "Zrób osobisty",
"resourceLauncherFilter": "Filtr",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sortuj",
"resourceLauncherSortAscending": "Sortuj rosnąco",
"resourceLauncherSortDescending": "Sortuj malejąco",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Grupuj według",
"resourceLauncherGroupBySite": "Witryna",
"resourceLauncherGroupByLabel": "Etykieta",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Układ",
"resourceLauncherLayoutGrid": "Siatka",
"resourceLauncherLayoutList": "Lista",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Pokaż tagi stron",
"resourceLauncherShowRecents": "Pokaż ostatnie",
"resourceLauncherDeleteView": "Usuń Widok",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Przeglądaj jako Administrator",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Pokaż szczegóły tego zasobu.",
"resourceLauncherUnlabeled": "Bez etykiety",
"resourceLauncherNoSite": "Brak strony",
"resourceLauncherNoResourcesInGroup": "W tej grupie nie ma zasobów",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Nie znaleziono zasobów",
"resourceLauncherEmptyStateNoResultsDescription": "Żadne zasoby nie spełniają twojego bieżącego wyszukiwania lub filtrów. Spróbuj je dostosować, aby znaleźć to, czego szukasz.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Żadne zasoby nie odpowiadają \"{query}\". Spróbuj dostosować swoje wyszukiwanie lub usunąć filtry, aby zobaczyć wszystkie zasoby.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Skopiowano do schowka",
"resourceLauncherCopiedAccessDescription": "Dostęp do zasobu został skopiowany do schowka.",
"resourceLauncherViewNamePlaceholder": "Nazwa widoku",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Autenticação de Cabeçalho Básico",
"policyAuthHeaderAuthDescription": "Valide um nome e valor de cabeçalho HTTP personalizado em cada solicitação",
"policyAuthHeaderAuthSummary": "Cabeçalho configurado",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Nome do Cabeçalho",
"policyAuthHeaderValue": "Valor esperado",
"policyAuthSetPasscode": "Definir Código de Acesso",
"policyAuthSetPincode": "Definir Código PIN",
"policyAuthSetEmailWhitelist": "Definir Lista de E-mails Permitidos",
@@ -1915,9 +1915,6 @@
"billingDomains": "Domínios",
"billingOrganizations": "Órgãos",
"billingRemoteExitNodes": "Nós remotos",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Nenhum limite configurado",
"billingEstimatedPeriod": "Período Estimado de Cobrança",
"billingIncludedUsage": "Uso Incluído",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Quantos usuários você pode usar",
"billingDomainInfo": "Quantos domínios você pode usar",
"billingRemoteExitNodesInfo": "Quantos nós remotos você pode usar",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Chaves de Licença",
"billingLicenseKeysDescription": "Gerenciar suas subscrições de chave de licença",
"billingLicenseSubscription": "Assinatura de Licença",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Recurso Desativado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
"resourceLauncherTitle": "Lançador de Recursos",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Veja os detalhes do recurso e lance-os de um só lugar",
"resourceLauncherSearchPlaceholder": "Procurar todos os sites...",
"resourceLauncherDefaultView": "Padrão",
"resourceLauncherSaveView": "Salvar Visualização",
"resourceLauncherSaveToCurrentView": "Salvar na Visualização Atual",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Redefinir Visualização",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Salvar como Nova Visualização",
"resourceLauncherSaveAsNewViewDescription": "Dê um nome a esta visualização para salvar os filtros e layout atuais.",
"resourceLauncherSaveForEveryone": "Salvar para Todos",
"resourceLauncherSaveForEveryoneDescription": "Compartilhe esta visualização com todos os membros da organização. Quando desmarcado, a visualização é visível apenas para você.",
"resourceLauncherMakePersonal": "Tornar Pessoal",
"resourceLauncherFilter": "Filtro",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Ordenar",
"resourceLauncherSortAscending": "Ordenar ascendente",
"resourceLauncherSortDescending": "Ordenar descendente",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Agrupar por",
"resourceLauncherGroupBySite": "Site",
"resourceLauncherGroupByLabel": "Marcador",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Layout",
"resourceLauncherLayoutGrid": "Grade",
"resourceLauncherLayoutList": "Lista",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Mostrar Etiquetas de Site",
"resourceLauncherShowRecents": "Mostrar Recents",
"resourceLauncherDeleteView": "Excluir Visualização",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Visualizar como Administrador",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Veja detalhes deste recurso.",
"resourceLauncherUnlabeled": "Sem Etiqueta",
"resourceLauncherNoSite": "Sem Site",
"resourceLauncherNoResourcesInGroup": "Nenhum recurso neste grupo",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Nenhum Recurso Encontrado",
"resourceLauncherEmptyStateNoResultsDescription": "Nenhum recurso corresponde à sua busca ou filtros atuais. Experimente ajustá-los para encontrar o que está procurando.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Nenhum recurso corresponde a \"{query}\". Tente ajustar sua busca ou limpar os filtros para ver todos os recursos.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Copiado para a área de transferência",
"resourceLauncherCopiedAccessDescription": "O acesso ao recurso foi copiado para sua área de transferência.",
"resourceLauncherViewNamePlaceholder": "Nome da Visualização",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Базовая аутентификация заголовка",
"policyAuthHeaderAuthDescription": "Проверка пользовательского имени и значения HTTP-заголовка для каждого запроса",
"policyAuthHeaderAuthSummary": "Заголовок настроен",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Имя заголовка",
"policyAuthHeaderValue": "Ожидаемое значение",
"policyAuthSetPasscode": "Установить пароль",
"policyAuthSetPincode": "Установить ПИН-код",
"policyAuthSetEmailWhitelist": "Установить белый список email",
@@ -1915,9 +1915,6 @@
"billingDomains": "Домены",
"billingOrganizations": "Орги",
"billingRemoteExitNodes": "Удаленные узлы",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Лимит не установлен",
"billingEstimatedPeriod": "Предполагаемый период выставления счетов",
"billingIncludedUsage": "Включенное использование",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Сколько пользователей вы можете использовать",
"billingDomainInfo": "Сколько доменов вы можете использовать",
"billingRemoteExitNodesInfo": "Сколько удаленных узлов вы можете использовать",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Лицензионные ключи",
"billingLicenseKeysDescription": "Управление подписками на лицензионные ключи",
"billingLicenseSubscription": "Лицензионное соглашение",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Ресурс отключён",
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
"resourceLauncherTitle": "Запуск ресурса",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Просмотр деталей ресурса и запуск их из одного места",
"resourceLauncherSearchPlaceholder": "Поиск всех сайтов...",
"resourceLauncherDefaultView": "По умолчанию",
"resourceLauncherSaveView": "Сохранить вид",
"resourceLauncherSaveToCurrentView": "Сохранить в текущий вид",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Сбросить вид",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Сохранить как новый вид",
"resourceLauncherSaveAsNewViewDescription": "Дайте этому виду имя, чтобы сохранить текущие фильтры и макет.",
"resourceLauncherSaveForEveryone": "Сохранить для всех",
"resourceLauncherSaveForEveryoneDescription": "Поделитесь этим видом со всеми членами организации. Если не отмечено, видимость только для вас.",
"resourceLauncherMakePersonal": "Сделать личным",
"resourceLauncherFilter": "Фильтр",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Сортировать",
"resourceLauncherSortAscending": "Сортировать по возрастанию",
"resourceLauncherSortDescending": "Сортировать по убыванию",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Группировать по",
"resourceLauncherGroupBySite": "Сайт",
"resourceLauncherGroupByLabel": "Метка",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Макет",
"resourceLauncherLayoutGrid": "Сетка",
"resourceLauncherLayoutList": "Список",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Показать теги сайта",
"resourceLauncherShowRecents": "Показать недавно",
"resourceLauncherDeleteView": "Удалить вид",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Просмотр как администратор",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Просмотр деталей этого ресурса.",
"resourceLauncherUnlabeled": "Без меток",
"resourceLauncherNoSite": "Без сайта",
"resourceLauncherNoResourcesInGroup": "Нет ресурсов в данной группе",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Ресурсы не найдены",
"resourceLauncherEmptyStateNoResultsDescription": "Ни один ресурс не соответствует вашему текущему поисковому запросу или фильтрам. Попробуйте их изменить, чтобы найти нужное.",
"resourceLauncherEmptyStateNoResultsWithQuery": "Ни один ресурс не соответствует \"{query}\". Попробуйте изменить параметры поиска или очистить фильтры, чтобы увидеть все ресурсы.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Скопировано в буфер обмена",
"resourceLauncherCopiedAccessDescription": "Доступ к ресурсу был скопирован в ваш буфер обмена.",
"resourceLauncherViewNamePlaceholder": "Имя вида",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "Temel Başlık Kimlik Doğrulama",
"policyAuthHeaderAuthDescription": "Her istekte özel bir HTTP başlık adını ve değerini doğrulayın",
"policyAuthHeaderAuthSummary": "Başlık yapılandırıldı",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "Başlık adı",
"policyAuthHeaderValue": "Beklenen değer",
"policyAuthSetPasscode": "Şifreyi Ayarla",
"policyAuthSetPincode": "PIN Kodunu Ayarla",
"policyAuthSetEmailWhitelist": "E-posta Beyaz Listesini Ayarla",
@@ -1915,9 +1915,6 @@
"billingDomains": "Alan Adları",
"billingOrganizations": "Organizasyonlar",
"billingRemoteExitNodes": "Uzak Düğümler",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
"billingEstimatedPeriod": "Tahmini Fatura Dönemi",
"billingIncludedUsage": "Dahil Kullanım",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "Kaç tane kullanıcı kullanabileceğiniz",
"billingDomainInfo": "Kaç tane alan adı kullanabileceğiniz",
"billingRemoteExitNodesInfo": "Kaç tane uzaktan düğüm kullanabileceğiniz",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "Lisans Anahtarları",
"billingLicenseKeysDescription": "Lisans anahtarı aboneliklerinizi yönetin",
"billingLicenseSubscription": "Lisans Aboneliği",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
"resourceLauncherTitle": "Kaynak Başlatıcı",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "Kaynağın detaylarını görüntüleyin ve tek bir yerden başlatın",
"resourceLauncherSearchPlaceholder": "Tüm siteleri ara...",
"resourceLauncherDefaultView": "Varsayılan",
"resourceLauncherSaveView": "Görünümü Kaydet",
"resourceLauncherSaveToCurrentView": "Mevcut Görünüme Kaydet",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "Görünümü Sıfırla",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "Yeni Görünüm Olarak Kaydet",
"resourceLauncherSaveAsNewViewDescription": "Geçerli filtrelerinizi ve düzeninizi kaydetmek için bu görünüme bir ad verin.",
"resourceLauncherSaveForEveryone": "Herkes İçin Kaydet",
"resourceLauncherSaveForEveryoneDescription": "Bu görünümü tüm kuruluş üyeleriyle paylaşın. İşaretli değilse, görünüm yalnızca size görünür olur.",
"resourceLauncherMakePersonal": "Kişisel Yap",
"resourceLauncherFilter": "Filtre",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "Sıralama",
"resourceLauncherSortAscending": "Artan sırala",
"resourceLauncherSortDescending": "Azalan sırala",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "Grupla",
"resourceLauncherGroupBySite": "Site",
"resourceLauncherGroupByLabel": "Etiket",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "Düzen",
"resourceLauncherLayoutGrid": "Izgara",
"resourceLauncherLayoutList": "Liste",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "Site Etiketlerini Göster",
"resourceLauncherShowRecents": "Son Eklenenleri Göster",
"resourceLauncherDeleteView": "Görünümü Sil",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "Yönetici Olarak Görüntüle",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "Bu kaynağın detaylarını görüntüleyin.",
"resourceLauncherUnlabeled": "Etiketsiz",
"resourceLauncherNoSite": "Site Yok",
"resourceLauncherNoResourcesInGroup": "Bu grupta kaynak yok",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "Kaynak Bulunamadı",
"resourceLauncherEmptyStateNoResultsDescription": "Mevcut arama veya filtrelerinizle eşleşen kaynak yok. Aradığınızı bulmak için ayarları değiştirmeyi deneyin.",
"resourceLauncherEmptyStateNoResultsWithQuery": "\"{query}\" ile eşleşen kaynak yok. Tüm kaynakları görmek için aramayı düzenlemeyi veya filtreleri temizlemeyi deneyin.",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "Panoya kopyalandı",
"resourceLauncherCopiedAccessDescription": "Kaynağa erişim panonuza kopyalandı.",
"resourceLauncherViewNamePlaceholder": "Görünüm adı",
+5 -35
View File
@@ -864,8 +864,8 @@
"policyAuthHeaderAuthTitle": "基础标题认证",
"policyAuthHeaderAuthDescription": "在每次请求上验证自定义 HTTP 标头名称和值",
"policyAuthHeaderAuthSummary": "标头已配置",
"policyAuthHeaderName": "Username",
"policyAuthHeaderValue": "Password",
"policyAuthHeaderName": "标头名称",
"policyAuthHeaderValue": "预期值",
"policyAuthSetPasscode": "设定密码",
"policyAuthSetPincode": "设置 PIN 码",
"policyAuthSetEmailWhitelist": "设置电子邮件白名单",
@@ -1915,9 +1915,6 @@
"billingDomains": "域",
"billingOrganizations": "球队",
"billingRemoteExitNodes": "远程节点",
"billingPublicResources": "Public Resources",
"billingPrivateResources": "Private Resources",
"billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "未配置限制",
"billingEstimatedPeriod": "估计结算周期",
"billingIncludedUsage": "包含的使用量",
@@ -1946,9 +1943,6 @@
"billingUsersInfo": "您可以使用多少用户",
"billingDomainInfo": "您可以使用多少域",
"billingRemoteExitNodesInfo": "您可以使用多少远程节点",
"billingPublicResourcesInfo": "How many public resources you can use",
"billingPrivateResourcesInfo": "How many private resources you can use",
"billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "许可证密钥",
"billingLicenseKeysDescription": "管理您的许可证密钥订阅",
"billingLicenseSubscription": "许可订阅",
@@ -3577,23 +3571,18 @@
"memberPortalResourceDisabled": "资源已禁用",
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
"resourceLauncherTitle": "资源启动器",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDescription": "查看资源详情并从一个地方启动它们",
"resourceLauncherSearchPlaceholder": "搜索所有站点……",
"resourceLauncherDefaultView": "默认",
"resourceLauncherSaveView": "保存视图",
"resourceLauncherSaveToCurrentView": "保存至当前视图",
"resourceLauncherSaveDefaultPersonal": "Save for Me",
"resourceLauncherResetView": "重置视图",
"resourceLauncherResetSystemDefault": "Reset to System Default",
"resourceLauncherSystemDefaultRestored": "System default restored",
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
"resourceLauncherSaveAsNewView": "另存为新视图",
"resourceLauncherSaveAsNewViewDescription": "为此视图命名,以便保存您当前的过滤器和布局。",
"resourceLauncherSaveForEveryone": "为所有人保存",
"resourceLauncherSaveForEveryoneDescription": "与所有组织成员共享此视图。如果未选中,此视图仅对您可见。",
"resourceLauncherMakePersonal": "创建个人",
"resourceLauncherFilter": "筛选",
"resourceLauncherFilterWithCount": "Filter, {count} applied",
"resourceLauncherSort": "排序",
"resourceLauncherSortAscending": "按升序排序",
"resourceLauncherSortDescending": "按降序排序",
@@ -3601,7 +3590,6 @@
"resourceLauncherGroupBy": "按组",
"resourceLauncherGroupBySite": "站点",
"resourceLauncherGroupByLabel": "标签",
"resourceLauncherGroupByNone": "None",
"resourceLauncherLayout": "布局",
"resourceLauncherLayoutGrid": "网格",
"resourceLauncherLayoutList": "列表",
@@ -3609,20 +3597,8 @@
"resourceLauncherShowSiteTags": "显示站点标签",
"resourceLauncherShowRecents": "显示最近使用",
"resourceLauncherDeleteView": "删除视图",
"resourceLauncherDeleteViewTitle": "Delete View",
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
"resourceLauncherDeleteViewConfirm": "Delete View",
"resourceLauncherViewAsAdmin": "以管理员身份查看",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherResourceDetailsDescription": "查看此资源的详细信息。",
"resourceLauncherUnlabeled": "未标记",
"resourceLauncherNoSite": "无站点",
"resourceLauncherNoResourcesInGroup": "此组中没有资源",
@@ -3631,12 +3607,6 @@
"resourceLauncherEmptyStateNoResultsTitle": "未找到资源",
"resourceLauncherEmptyStateNoResultsDescription": "没有资源与您当前的搜索或过滤器匹配。尝试调整它们以找到您想要的内容。",
"resourceLauncherEmptyStateNoResultsWithQuery": "没有资源匹配\"{query}\"。尝试调整您的搜索或清除过滤器以查看所有资源。",
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
"resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
"resourceLauncherCopiedToClipboard": "已复制到剪贴板",
"resourceLauncherCopiedAccessDescription": "资源访问权限已复制到剪贴板。",
"resourceLauncherViewNamePlaceholder": "查看名称",
+6
View File
@@ -27,6 +27,12 @@ export async function getFeatureDisplayName(
return "Remote Exit Nodes";
case LimitId.ORGANIZATIONS:
return "Organizations";
case LimitId.PUBLIC_RESOURCES:
return "Public Resources";
case LimitId.PRIVATE_RESOURCES:
return "Private Resources";
case LimitId.MACHINE_CLIENTS:
return "Machine Clients";
case LimitId.TIER1:
return "Home Lab";
default:
+83
View File
@@ -0,0 +1,83 @@
import logger from "@server/logger";
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 50;
/**
* Detect transient errors that are safe to retry (connection drops, deadlocks,
* serialization failures). PostgreSQL deadlocks (40P01) are always safe to
* retry: the database guarantees exactly one winner per deadlock pair, so the
* loser just needs to try again.
*/
export function isTransientError(error: any): boolean {
if (!error) return false;
const message = (error.message || "").toLowerCase();
const causeMessage = (error.cause?.message || "").toLowerCase();
const code = error.code || error.cause?.code || "";
// Connection timeout / terminated
if (
message.includes("connection timeout") ||
message.includes("connection terminated") ||
message.includes("timeout exceeded when trying to connect") ||
causeMessage.includes("connection terminated unexpectedly") ||
causeMessage.includes("connection timeout")
) {
return true;
}
// PostgreSQL deadlock detected - always safe to retry (one winner guaranteed)
if (code === "40P01" || message.includes("deadlock")) {
return true;
}
// PostgreSQL serialization failure
if (code === "40001") {
return true;
}
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
code === "EPIPE" ||
code === "ETIMEDOUT"
) {
return true;
}
return false;
}
/**
* Simple retry wrapper with exponential backoff for transient errors
* (deadlocks, connection timeouts, unexpected disconnects).
*/
export async function withRetry<T>(
operation: () => Promise<T>,
context: string,
maxRetries: number = MAX_RETRIES,
baseDelayMs: number = BASE_DELAY_MS
): Promise<T> {
let attempt = 0;
while (true) {
try {
return await operation();
} catch (error: any) {
if (isTransientError(error) && attempt < maxRetries) {
attempt++;
const baseDelay = Math.pow(2, attempt - 1) * baseDelayMs;
const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter;
logger.warn(
`Transient DB error in ${context}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`,
{ code: error?.code ?? error?.cause?.code }
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
+49 -24
View File
@@ -1,30 +1,55 @@
import { db, exitNodes } from "@server/db";
import { db, exitNodes, Transaction } from "@server/db";
import config from "@server/lib/config";
import { findNextAvailableCidr } from "@server/lib/ip";
import { lockManager } from "#dynamic/lib/lock";
export async function getNextAvailableSubnet(): Promise<string> {
// Get all existing subnets from routes table
const existingAddresses = await db
.select({
address: exitNodes.address
})
.from(exitNodes);
const addresses = existingAddresses.map((a) => a.address);
let subnet = findNextAvailableCidr(
addresses,
config.getRawConfig().gerbil.block_size,
config.getRawConfig().gerbil.subnet_group
);
if (!subnet) {
throw new Error("No available subnets remaining in space");
/**
* Reserves the next available exit node subnet.
*
* Exit node subnets must never overlap with one another - regardless of
* which org(s) they belong to - since HA exit nodes can end up routing for
* the same org. This acquires a lock that the caller MUST release (via the
* returned `release`) only after the chosen address has been durably
* persisted (e.g. after the enclosing transaction commits), otherwise
* concurrent callers can race and pick the same subnet.
*/
export async function getNextAvailableSubnet(
trx: Transaction | typeof db = db
): Promise<{ value: string; release: () => Promise<void> }> {
const lockKey = "exit-node-subnet-allocation";
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
const release = () => lockManager.releaseLock(lockKey, acquired);
// replace the last octet with 1
subnet =
subnet.split(".").slice(0, 3).join(".") +
".1" +
"/" +
subnet.split("/")[1];
return subnet;
try {
// Get all existing subnets from routes table
const existingAddresses = await trx
.select({
address: exitNodes.address
})
.from(exitNodes);
const addresses = existingAddresses.map((a) => a.address);
let subnet = findNextAvailableCidr(
addresses,
config.getRawConfig().gerbil.block_size,
config.getRawConfig().gerbil.subnet_group
);
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
// replace the last octet with 1
subnet =
subnet.split(".").slice(0, 3).join(".") +
".1" +
"/" +
subnet.split("/")[1];
return { value: subnet, release };
} catch (e) {
await release();
throw e;
}
}
+201 -127
View File
@@ -45,6 +45,7 @@ import {
} from "@server/routers/client/targets";
import { lockManager } from "#dynamic/lib/lock";
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
import { withRetry, isTransientError } from "@server/lib/dbRetry";
import {
checkOrgRebuildRateLimit,
decrementOrgRebuildCount,
@@ -285,10 +286,20 @@ export async function rebuildClientAssociationsFromSiteResource(
) {
await incrementOrgRebuildCount(siteResource.orgId);
try {
return await lockManager.withLock(
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource),
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
// The whole locked rebuild is idempotent (it diffs full expected vs.
// actual state each time), so on a transient DB error it's safe to
// retry the entire thing rather than just the failed query.
return await withRetry(
() =>
lockManager.withLock(
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
() =>
rebuildClientAssociationsFromSiteResourceImpl(
siteResource
),
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
),
`rebuildClientAssociationsFromSiteResource:${siteResource.siteResourceId}`
);
} catch (err: any) {
if (
@@ -304,6 +315,17 @@ export async function rebuildClientAssociationsFromSiteResource(
});
return { mergedAllClients: [] };
}
if (isTransientError(err)) {
logger.warn(
`rebuildClientAssociations: transient DB error rebuilding site resource ${siteResource.siteResourceId} persisted after retries, queuing for deferred processing:`,
err
);
await rebuildQueue.enqueue({
type: "site-resource",
id: siteResource.siteResourceId
});
return { mergedAllClients: [] };
}
throw err;
} finally {
await decrementOrgRebuildCount(siteResource.orgId);
@@ -463,6 +485,7 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
await trx
.insert(clientSiteResourcesAssociationsCache)
.values(clientSiteResourcesToInsert)
.onConflictDoNothing()
.returning();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} inserted clientSiteResource associations`
@@ -510,121 +533,148 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
for (const site of sitesToProcess) {
const siteId = site.siteId;
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] processing siteId=${siteId} for siteResourceId=${siteResource.siteResourceId}`
);
try {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] processing siteId=${siteId} for siteResourceId=${siteResource.siteResourceId}`
);
const existingClientSites = await trx
.select({
clientId: clientSitesAssociationsCache.clientId
})
.from(clientSitesAssociationsCache)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
const existingClientSites = await trx
.select({
clientId: clientSitesAssociationsCache.clientId
})
.from(clientSitesAssociationsCache)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
const existingClientSiteIds = existingClientSites.map(
(row) => row.clientId
);
const existingClientSiteIds = existingClientSites.map(
(row) => row.clientId
);
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} existingClientSiteIds=[${existingClientSiteIds.join(", ")}]`
);
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} existingClientSiteIds=[${existingClientSiteIds.join(", ")}]`
);
// Get full client details for existing clients (needed for sending delete messages)
const existingClients =
existingClientSiteIds.length > 0
? await trx
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.where(inArray(clients.clientId, existingClientSiteIds))
// Get full client details for existing clients (needed for sending delete messages)
const existingClients =
existingClientSiteIds.length > 0
? await trx
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.where(
inArray(clients.clientId, existingClientSiteIds)
)
: [];
const otherResourceClientIds =
clientsFromOtherResourcesBySite.get(siteId) ??
new Set<number>();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
);
// Expected clients from this resource are site-scoped: if this site is
// no longer attached to the resource, the expected set is empty.
const expectedClientIdsForSite = currentSiteIdSet.has(siteId)
? mergedAllClientIds
: [];
const otherResourceClientIds =
clientsFromOtherResourcesBySite.get(siteId) ?? new Set<number>();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
);
// Expected clients from this resource are site-scoped: if this site is
// no longer attached to the resource, the expected set is empty.
const expectedClientIdsForSite = currentSiteIdSet.has(siteId)
? mergedAllClientIds
: [];
const clientSitesToAdd = expectedClientIdsForSite.filter(
(clientId) =>
!existingClientSiteIds.includes(clientId) &&
!otherResourceClientIds.has(clientId) // dont add if already connected via another site resource
);
const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
clientId,
siteId
}));
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toAdd=[${clientSitesToAdd.join(", ")}]`
);
if (clientSitesToInsert.length > 0) {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserting ${clientSitesToInsert.length} clientSite association(s)`
// Note: we deliberately do NOT exclude clients covered by another
// site resource here (unlike clientSitesToRemove below). Doing so
// previously caused a permanent gap: if resource A saw resource B's
// cache row and skipped adding (assuming B would maintain it), and
// B's own rebuild made the same assumption about A, the site-level
// row could end up never inserted by anyone even though both
// resources' client associations were otherwise correct.
// onConflictDoNothing makes a redundant insert harmless, so there's
// no correctness reason to skip here.
const clientSitesToAdd = expectedClientIdsForSite.filter(
(clientId) => !existingClientSiteIds.includes(clientId)
);
await trx
.insert(clientSitesAssociationsCache)
.values(clientSitesToInsert)
.returning();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserted clientSite associations`
);
} else {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} no clientSite associations to insert`
);
}
// Now remove any client-site associations that should no longer exist
const clientSitesToRemove = existingClientSiteIds.filter(
(clientId) =>
!expectedClientIdsForSite.includes(clientId) &&
!otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
);
const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
clientId,
siteId
}));
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toRemove=[${clientSitesToRemove.join(", ")}]`
);
if (clientSitesToRemove.length > 0) {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} deleting ${clientSitesToRemove.length} clientSite association(s)`
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toAdd=[${clientSitesToAdd.join(", ")}]`
);
await trx
.delete(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.siteId, siteId),
inArray(
clientSitesAssociationsCache.clientId,
clientSitesToRemove
)
)
if (clientSitesToInsert.length > 0) {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserting ${clientSitesToInsert.length} clientSite association(s)`
);
}
await trx
.insert(clientSitesAssociationsCache)
.values(clientSitesToInsert)
.onConflictDoNothing()
.returning();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserted clientSite associations`
);
} else {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} no clientSite associations to insert`
);
}
// Now handle the messages to add/remove peers on both the newt and olm sides
await handleMessagesForSiteClients(
site,
siteId,
mergedAllClients,
existingClients,
clientSitesToAdd,
clientSitesToRemove,
trx
);
// Now remove any client-site associations that should no longer exist
const clientSitesToRemove = existingClientSiteIds.filter(
(clientId) =>
!expectedClientIdsForSite.includes(clientId) &&
!otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
);
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toRemove=[${clientSitesToRemove.join(", ")}]`
);
if (clientSitesToRemove.length > 0) {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} deleting ${clientSitesToRemove.length} clientSite association(s)`
);
await trx
.delete(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.siteId, siteId),
inArray(
clientSitesAssociationsCache.clientId,
clientSitesToRemove
)
)
);
}
// Now handle the messages to add/remove peers on both the newt and olm sides
await handleMessagesForSiteClients(
site,
siteId,
mergedAllClients,
existingClients,
clientSitesToAdd,
clientSitesToRemove,
trx
);
} catch (err) {
// Don't let a failure on one site abort processing of every
// other site queued after it in this run. Since we're not
// re-throwing, the outer wrapper's retry/requeue logic never
// sees this failure, so explicitly queue this resource for a
// follow-up pass to reconcile whatever this site didn't get to.
logger.error(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} failed while processing site for siteResourceId=${siteResource.siteResourceId}, continuing with remaining sites and queuing a follow-up pass:`,
err
);
await rebuildQueue.enqueue({
type: "site-resource",
id: siteResource.siteResourceId
});
}
}
// Handle subnet proxy target updates for the resource associations
@@ -657,7 +707,7 @@ async function handleMessagesForSiteClients(
trx: Transaction | typeof db = db
): Promise<void> {
if (!site.exitNodeId) {
logger.warn(
logger.debug(
`Exit node ID not on site ${site.siteId} so there is no reason to update clients because it must be offline`
);
return;
@@ -671,14 +721,14 @@ async function handleMessagesForSiteClients(
.limit(1);
if (!exitNode) {
logger.warn(
logger.debug(
`Exit node not found for site ${site.siteId} so there is no reason to update clients because it must be offline`
);
return;
}
if (!site.publicKey) {
logger.warn(
logger.debug(
`Site publicKey not set for site ${site.siteId} so cannot add peers to clients`
);
return;
@@ -692,7 +742,7 @@ async function handleMessagesForSiteClients(
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
logger.warn(
logger.debug(
`Newt not found for site ${siteId} so cannot add peers to clients`
);
return;
@@ -917,7 +967,7 @@ export async function updateClientSiteDestinations(
for (const site of sitesData) {
if (!site.sites.subnet) {
logger.warn(`Site ${site.sites.siteId} has no subnet, skipping`);
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
continue;
}
@@ -1656,10 +1706,17 @@ export async function rebuildClientAssociationsFromClient(
await incrementOrgRebuildCount(client.orgId);
try {
const trx = primaryDb;
return await lockManager.withLock(
`rebuild-client-associations:client:${client.clientId}`,
() => rebuildClientAssociationsFromClientImpl(client, trx),
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
// The whole locked rebuild is idempotent (it diffs full expected vs.
// actual state each time), so on a transient DB error it's safe to
// retry the entire thing rather than just the failed query.
return await withRetry(
() =>
lockManager.withLock(
`rebuild-client-associations:client:${client.clientId}`,
() => rebuildClientAssociationsFromClientImpl(client, trx),
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
),
`rebuildClientAssociationsFromClient:${client.clientId}`
);
} catch (err: any) {
if (
@@ -1675,6 +1732,17 @@ export async function rebuildClientAssociationsFromClient(
});
return;
}
if (isTransientError(err)) {
logger.warn(
`rebuildClientAssociations: transient DB error rebuilding client ${client.clientId} persisted after retries, queuing for deferred processing:`,
err
);
await rebuildQueue.enqueue({
type: "client",
id: client.clientId
});
return;
}
throw err;
} finally {
await decrementOrgRebuildCount(client.orgId);
@@ -1826,12 +1894,15 @@ async function rebuildClientAssociationsFromClientImpl(
// Insert new associations
if (resourcesToAdd.length > 0) {
await trx.insert(clientSiteResourcesAssociationsCache).values(
resourcesToAdd.map((siteResourceId) => ({
clientId: client.clientId,
siteResourceId
}))
);
await trx
.insert(clientSiteResourcesAssociationsCache)
.values(
resourcesToAdd.map((siteResourceId) => ({
clientId: client.clientId,
siteResourceId
}))
)
.onConflictDoNothing();
}
// Remove old associations
@@ -1869,12 +1940,15 @@ async function rebuildClientAssociationsFromClientImpl(
// Insert new site associations
if (sitesToAdd.length > 0) {
await trx.insert(clientSitesAssociationsCache).values(
sitesToAdd.map((siteId) => ({
clientId: client.clientId,
siteId
}))
);
await trx
.insert(clientSitesAssociationsCache)
.values(
sitesToAdd.map((siteId) => ({
clientId: client.clientId,
siteId
}))
)
.onConflictDoNothing();
}
// Remove old site associations
+31 -4
View File
@@ -1,10 +1,14 @@
import logger from "@server/logger";
import { isTransientError } from "@server/lib/dbRetry";
export type RebuildJobType = "site-resource" | "client";
export interface RebuildJob {
type: RebuildJobType;
id: number;
// Number of times this job has already been re-queued after a transient
// failure. Absent/0 means it has not failed yet.
attempt?: number;
}
export interface RebuildJobHandlers {
@@ -24,6 +28,10 @@ export interface RebuildQueueManager {
// retried shortly after against fresh DB state.
const POLL_INTERVAL_MS = 500;
const BATCH_SIZE = 5;
// A job that fails with a transient DB error gets re-queued with backoff
// instead of being dropped, up to this many times.
const MAX_JOB_ATTEMPTS = 5;
const JOB_RETRY_BASE_DELAY_MS = 1000;
function dedupeKey(job: RebuildJob): string {
return `${job.type}:${job.id}`;
@@ -106,10 +114,29 @@ class InMemoryRebuildQueue implements RebuildQueueManager {
`Rebuild queue: completed ${job.type}:${job.id}`
);
} catch (err) {
logger.error(
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
err
);
const attempt = (job.attempt ?? 0) + 1;
if (isTransientError(err) && attempt <= MAX_JOB_ATTEMPTS) {
const delay =
JOB_RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
logger.warn(
`Rebuild queue: job ${job.type}:${job.id} hit a transient error (attempt ${attempt}/${MAX_JOB_ATTEMPTS}), re-queuing in ${delay}ms:`,
err
);
setTimeout(() => {
this.enqueue({ ...job, attempt }).catch(
(enqueueErr) =>
logger.error(
`Rebuild queue: failed to re-queue ${job.type}:${job.id} after transient error:`,
enqueueErr
)
);
}, delay);
} else {
logger.error(
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
err
);
}
}
}
} finally {
+1 -1
View File
@@ -14,7 +14,7 @@
import { redis } from "#private/lib/redis";
import logger from "@server/logger";
export const ORG_REBUILD_CONCURRENCY_LIMIT = 5;
export const ORG_REBUILD_CONCURRENCY_LIMIT = 10;
// Safety-net TTL: slightly longer than the rebuild lock TTL (120 s). If a
// server process dies while holding a rebuild, this ensures the counter key
+36 -4
View File
@@ -14,12 +14,16 @@
import { redis } from "#private/lib/redis";
import { lockManager } from "#private/lib/lock";
import logger from "@server/logger";
import { isTransientError } from "@server/lib/dbRetry";
export type RebuildJobType = "site-resource" | "client";
export interface RebuildJob {
type: RebuildJobType;
id: number;
// Number of times this job has already been re-queued after a transient
// failure. Absent/0 means it has not failed yet.
attempt?: number;
}
export interface RebuildJobHandlers {
@@ -43,6 +47,11 @@ const PROCESSOR_LOCK_TTL_MS = 120000 * BATCH_SIZE + 30000; // ~630 s
const POLL_INTERVAL_MS = 500;
// A job that fails with a transient DB error gets re-queued with backoff
// instead of being dropped, up to this many times.
const MAX_JOB_ATTEMPTS = 5;
const JOB_RETRY_BASE_DELAY_MS = 1000;
class RedisRebuildQueue {
private processingStarted = false;
@@ -180,10 +189,33 @@ class RedisRebuildQueue {
`Rebuild queue: completed ${job.type}:${job.id}`
);
} catch (err) {
logger.error(
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
err
);
const attempt = (job.attempt ?? 0) + 1;
if (
isTransientError(err) &&
attempt <= MAX_JOB_ATTEMPTS
) {
const delay =
JOB_RETRY_BASE_DELAY_MS *
Math.pow(2, attempt - 1);
logger.warn(
`Rebuild queue: job ${job.type}:${job.id} hit a transient error (attempt ${attempt}/${MAX_JOB_ATTEMPTS}), re-queuing in ${delay}ms:`,
err
);
setTimeout(() => {
this.enqueue({ ...job, attempt }).catch(
(enqueueErr) =>
logger.error(
`Rebuild queue: failed to re-queue ${job.type}:${job.id} after transient error:`,
enqueueErr
)
);
}, delay);
} else {
logger.error(
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
err
);
}
}
}
},
+41 -18
View File
@@ -894,6 +894,19 @@ class RegionalRedisManager {
return opts;
}
// The regional Redis StatefulSet's "redis" service pins to pod redis-0
// (primary). The replica (redis-1) is only reachable through the
// per-pod headless service: <svc>.<namespace>.svc.cluster.local ->
// redis-1.redis-headless.<namespace>.svc.cluster.local. Returns null
// if the configured host doesn't match that pattern (e.g. local dev),
// in which case callers should fall back to the primary for reads.
private getReplicaHost(primaryHost: string): string | null {
const match = primaryHost.match(/^redis\.([^.]+)\.svc\.cluster\.local$/);
if (!match) return null;
const namespace = match[1];
return `redis-1.redis-headless.${namespace}.svc.cluster.local`;
}
private initializeClients(): void {
const cfg = this.getConfig();
const baseOpts = {
@@ -907,35 +920,42 @@ class RegionalRedisManager {
try {
this.writeClient = new Redis(baseOpts);
// redis-1 (replica) handles reads; fall back to primary if not resolvable
this.readClient = new Redis({
...baseOpts,
host: cfg.host!.replace(/^(.*?)(\.\S+)$/, (_, h, rest) => {
// Derive replica hostname from the headless service pattern:
// redis.redis.svc.cluster.local -> redis-1.redis-headless.redis.svc.cluster.local
// If it doesn't look like a k8s service, just use the same host
return h + rest;
})
});
// For simplicity use same host for both; callers can always read from primary
// The real replica routing is handled by the StatefulSet headless service
this.readClient = this.writeClient;
const replicaHost = this.getReplicaHost(cfg.host!);
this.readClient = replicaHost
? new Redis({ ...baseOpts, host: replicaHost })
: this.writeClient;
this.writeClient.on("ready", () => {
logger.info("Regional Redis client ready");
logger.info("Regional Redis write client ready");
this.isHealthy = true;
});
this.writeClient.on("error", (err) => {
logger.error("Regional Redis client error:", err);
logger.error("Regional Redis write client error:", err);
this.isHealthy = false;
});
this.writeClient.on("reconnecting", () => {
logger.info("Regional Redis client reconnecting...");
logger.info("Regional Redis write client reconnecting...");
this.isHealthy = false;
});
logger.info("Regional Redis client initialized");
if (this.readClient !== this.writeClient) {
this.readClient.on("ready", () => {
logger.info("Regional Redis read client ready");
});
this.readClient.on("error", (err) => {
logger.error("Regional Redis read client error:", err);
});
this.readClient.on("reconnecting", () => {
logger.info("Regional Redis read client reconnecting...");
});
}
logger.info(
replicaHost
? `Regional Redis client initialized (reads routed to replica ${replicaHost})`
: "Regional Redis client initialized (no replica resolvable, reads routed to primary)"
);
} catch (error) {
logger.error("Failed to initialize regional Redis client:", error);
this.isEnabled = false;
@@ -1041,11 +1061,14 @@ class RegionalRedisManager {
public async disconnect(): Promise<void> {
try {
if (this.readClient && this.readClient !== this.writeClient) {
await this.readClient.quit();
}
this.readClient = null;
if (this.writeClient) {
await this.writeClient.quit();
this.writeClient = null;
}
this.readClient = null;
logger.info("Regional Redis client disconnected");
} catch (error) {
logger.error("Error disconnecting regional Redis client:", error);
@@ -104,6 +104,18 @@ export async function getOrgUsage(
orgId,
LimitId.ORGANIZATIONS
);
const publicResources = await usageService.getUsage(
orgId,
LimitId.PUBLIC_RESOURCES
);
const privateResources = await usageService.getUsage(
orgId,
LimitId.PRIVATE_RESOURCES
);
const machineClients = await usageService.getUsage(
orgId,
LimitId.MACHINE_CLIENTS
);
// const egressData = await usageService.getUsage(
// orgId,
// FeatureId.EGRESS_DATA_MB
@@ -127,6 +139,15 @@ export async function getOrgUsage(
if (organizations) {
usageData.push(organizations);
}
if (publicResources) {
usageData.push(publicResources);
}
if (privateResources) {
usageData.push(privateResources);
}
if (machineClients) {
usageData.push(machineClients);
}
const orgLimits = await db
.select()
+34 -30
View File
@@ -29,37 +29,41 @@ export async function createExitNode(
.where(eq(exitNodes.publicKey, publicKey));
let exitNode: ExitNode;
if (!exitNodeQuery) {
const address = await getNextAvailableSubnet();
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
// const listenPort = await getNextAvailablePort();
const listenPort = config.getRawConfig().gerbil.start_port;
let subEndpoint = "";
if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName();
const { value: address, release } = await getNextAvailableSubnet();
try {
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
// const listenPort = await getNextAvailablePort();
const listenPort = config.getRawConfig().gerbil.start_port;
let subEndpoint = "";
if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName();
}
const exitNodeName =
config.getRawConfig().gerbil.exit_node_name ||
`Exit Node ${publicKey.slice(0, 8)}`;
// create a new exit node
[exitNode] = await db
.insert(exitNodes)
.values({
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
listenPort,
online: true,
reachableAt,
name: exitNodeName
})
.returning()
.execute();
logger.info(
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
);
} finally {
await release();
}
const exitNodeName =
config.getRawConfig().gerbil.exit_node_name ||
`Exit Node ${publicKey.slice(0, 8)}`;
// create a new exit node
[exitNode] = await db
.insert(exitNodes)
.values({
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
listenPort,
online: true,
reachableAt,
name: exitNodeName
})
.returning()
.execute();
logger.info(
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
);
} else {
// update the reachable at
[exitNode] = await db
@@ -114,8 +114,6 @@ export async function createRemoteExitNode(
}
const secretHash = await hashPassword(secret);
// const address = await getNextAvailableSubnet();
const address = "100.89.140.1/24"; // FOR NOW LETS HARDCODE THESE ADDRESSES
const [existingRemoteExitNode] = await db
.select()
@@ -191,89 +189,106 @@ export async function createRemoteExitNode(
);
}
await db.transaction(async (trx) => {
if (!existingExitNode) {
const [res] = await trx
.insert(exitNodes)
.values({
name: remoteExitNodeId,
address,
endpoint: "",
publicKey: "",
listenPort: 0,
online: false,
type: "remoteExitNode"
})
.returning();
existingExitNode = res;
}
// If this remote exit node isn't already backing an exit node in
// another org, we're about to create a brand new one. Reserve a
// subnet for it up front so the allocation lock is held across the
// whole insert - this guarantees exit node subnets never overlap,
// even under concurrent creation, which matters for HA setups.
let releaseSubnetLock: (() => Promise<void>) | null = null;
let newExitNodeAddress: string | null = null;
if (!existingExitNode) {
const { value, release } = await getNextAvailableSubnet();
newExitNodeAddress = value;
releaseSubnetLock = release;
}
if (!existingRemoteExitNode) {
await trx.insert(remoteExitNodes).values({
remoteExitNodeId: remoteExitNodeId,
secretHash,
dateCreated: moment().toISOString(),
exitNodeId: existingExitNode.exitNodeId
});
} else {
// update the existing remote exit node
await trx
.update(remoteExitNodes)
.set({
exitNodeId: existingExitNode.exitNodeId
})
.where(
eq(
remoteExitNodes.remoteExitNodeId,
existingRemoteExitNode.remoteExitNodeId
)
);
}
if (!existingExitNodeOrg) {
await trx.insert(exitNodeOrgs).values({
exitNodeId: existingExitNode.exitNodeId,
orgId: orgId
});
}
// calculate if the node is in any other of the orgs before we count it as an add to the billing org
if (org.billingOrgId) {
const otherBillingOrgs = await trx
.select()
.from(orgs)
.where(
and(
eq(orgs.billingOrgId, org.billingOrgId),
ne(orgs.orgId, orgId)
)
);
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
const orgsInBillingDomainThatTheNodeIsStillIn = await trx
.select()
.from(exitNodeOrgs)
.where(
and(
eq(
exitNodeOrgs.exitNodeId,
existingExitNode.exitNodeId
),
inArray(exitNodeOrgs.orgId, billingOrgIds)
)
);
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add(
orgId,
LimitId.REMOTE_EXIT_NODES,
1,
trx
);
try {
await db.transaction(async (trx) => {
if (!existingExitNode) {
const [res] = await trx
.insert(exitNodes)
.values({
name: remoteExitNodeId,
address: newExitNodeAddress!,
endpoint: "",
publicKey: "",
listenPort: 0,
online: false,
type: "remoteExitNode"
})
.returning();
existingExitNode = res;
}
}
});
if (!existingRemoteExitNode) {
await trx.insert(remoteExitNodes).values({
remoteExitNodeId: remoteExitNodeId,
secretHash,
dateCreated: moment().toISOString(),
exitNodeId: existingExitNode.exitNodeId
});
} else {
// update the existing remote exit node
await trx
.update(remoteExitNodes)
.set({
exitNodeId: existingExitNode.exitNodeId
})
.where(
eq(
remoteExitNodes.remoteExitNodeId,
existingRemoteExitNode.remoteExitNodeId
)
);
}
if (!existingExitNodeOrg) {
await trx.insert(exitNodeOrgs).values({
exitNodeId: existingExitNode.exitNodeId,
orgId: orgId
});
}
// calculate if the node is in any other of the orgs before we count it as an add to the billing org
if (org.billingOrgId) {
const otherBillingOrgs = await trx
.select()
.from(orgs)
.where(
and(
eq(orgs.billingOrgId, org.billingOrgId),
ne(orgs.orgId, orgId)
)
);
const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
const orgsInBillingDomainThatTheNodeIsStillIn = await trx
.select()
.from(exitNodeOrgs)
.where(
and(
eq(
exitNodeOrgs.exitNodeId,
existingExitNode.exitNodeId
),
inArray(exitNodeOrgs.orgId, billingOrgIds)
)
);
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add(
orgId,
LimitId.REMOTE_EXIT_NODES,
1,
trx
);
}
}
});
} finally {
await releaseSubnetLock?.();
}
const token = generateSessionToken();
await createRemoteExitNodeSession(token, remoteExitNodeId);
+34 -30
View File
@@ -13,37 +13,41 @@ export async function createExitNode(
const [exitNodeQuery] = await db.select().from(exitNodes).limit(1);
let exitNode: ExitNode;
if (!exitNodeQuery) {
const address = await getNextAvailableSubnet();
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
// const listenPort = await getNextAvailablePort();
const listenPort = config.getRawConfig().gerbil.start_port;
let subEndpoint = "";
if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName();
const { value: address, release } = await getNextAvailableSubnet();
try {
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
// const listenPort = await getNextAvailablePort();
const listenPort = config.getRawConfig().gerbil.start_port;
let subEndpoint = "";
if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName();
}
const exitNodeName =
config.getRawConfig().gerbil.exit_node_name ||
`Exit Node ${publicKey.slice(0, 8)}`;
// create a new exit node
[exitNode] = await db
.insert(exitNodes)
.values({
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
online: true,
listenPort,
reachableAt,
name: exitNodeName
})
.returning()
.execute();
logger.info(
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
);
} finally {
await release();
}
const exitNodeName =
config.getRawConfig().gerbil.exit_node_name ||
`Exit Node ${publicKey.slice(0, 8)}`;
// create a new exit node
[exitNode] = await db
.insert(exitNodes)
.values({
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
online: true,
listenPort,
reachableAt,
name: exitNodeName
})
.returning()
.execute();
logger.info(
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
);
} else {
// update the existing exit node
[exitNode] = await db
+2 -2
View File
@@ -52,13 +52,13 @@ export async function buildClientConfigurationForNewtClient(
clientsRes
.filter((client) => {
if (!client.clients.pubKey) {
logger.warn(
logger.debug(
`Client ${client.clients.clientId} has no public key, skipping`
);
return false;
}
if (!client.clients.subnet) {
logger.warn(
logger.debug(
`Client ${client.clients.clientId} has no subnet, skipping`
);
return false;
@@ -19,7 +19,7 @@ export const handleNewtDisconnectingMessage: MessageHandler = async (
}
if (!newt.siteId) {
logger.warn("Newt has no client ID!");
logger.warn("Newt has no site ID!");
return;
}
@@ -34,6 +34,12 @@ export const handleNewtDisconnectingMessage: MessageHandler = async (
.where(eq(sites.siteId, newt.siteId!))
.returning();
if (!site) {
throw new Error(
`Could not find site ${newt.siteId} to update disconnection from disconnect message`
);
}
await fireSiteOfflineAlert(
site.orgId,
site.siteId,
@@ -43,6 +49,6 @@ export const handleNewtDisconnectingMessage: MessageHandler = async (
);
});
} catch (error) {
logger.error("Error handling disconnecting message", { error });
logger.error("Error handling site disconnecting message", error);
}
};
+22 -10
View File
@@ -5,8 +5,21 @@ import { Newt } from "@server/db";
import { eq } from "drizzle-orm";
import logger from "@server/logger";
import { sendNewtSyncMessage } from "./sync";
import semver from "semver";
import { recordSitePing } from "./pingAccumulator";
const NEWT_SUPPORTS_SYNC_VERSION = ">=1.14.0";
const PONG = {
message: {
type: "pong",
data: {
timestamp: new Date().toISOString()
}
},
broadcast: false,
excludeSender: false
};
/**
* Handles ping messages from newt clients.
*
@@ -37,6 +50,14 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
// cross-region latency to the database.
recordSitePing(newt.siteId);
if (
newt.version &&
!semver.satisfies(newt.version, NEWT_SUPPORTS_SYNC_VERSION)
) {
// Newt does not support the sync message so not checking - stop here -
return PONG;
}
// Check config version and sync if stale.
const configVersion = await getClientConfigVersion(newt.newtId);
@@ -65,14 +86,5 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
await sendNewtSyncMessage(newt, site);
}
return {
message: {
type: "pong",
data: {
timestamp: new Date().toISOString()
}
},
broadcast: false,
excludeSender: false
};
return PONG;
};
+2 -81
View File
@@ -3,6 +3,7 @@ import { sites, clients, olms } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import logger from "@server/logger";
import { fireSiteOnlineAlert } from "@server/lib/alerts";
import { withRetry } from "@server/lib/dbRetry";
/**
* Ping Accumulator
@@ -22,8 +23,6 @@ import { fireSiteOnlineAlert } from "@server/lib/alerts";
*/
const FLUSH_INTERVAL_MS = 10_000; // Flush every 10 seconds
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 50;
// ── Site (newt) pings ──────────────────────────────────────────────────
// Map of siteId -> latest ping timestamp (unix seconds)
@@ -266,85 +265,7 @@ export async function flushPingsToDb(): Promise<void> {
}
// ── Retry / Error Helpers ──────────────────────────────────────────────
/**
* Simple retry wrapper with exponential backoff for transient errors
* (deadlocks, connection timeouts, unexpected disconnects).
*
* PostgreSQL deadlocks (40P01) are always safe to retry: the database
* guarantees exactly one winner per deadlock pair, so the loser just needs
* to try again. MAX_RETRIES is intentionally higher than typical connection
* retry budgets to give deadlock victims enough chances to succeed.
*/
async function withRetry<T>(
operation: () => Promise<T>,
context: string
): Promise<T> {
let attempt = 0;
while (true) {
try {
return await operation();
} catch (error: any) {
if (isTransientError(error) && attempt < MAX_RETRIES) {
attempt++;
const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS;
const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter;
logger.warn(
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`,
{ code: error?.code ?? error?.cause?.code }
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
/**
* Detect transient errors that are safe to retry.
*/
function isTransientError(error: any): boolean {
if (!error) return false;
const message = (error.message || "").toLowerCase();
const causeMessage = (error.cause?.message || "").toLowerCase();
const code = error.code || error.cause?.code || "";
// Connection timeout / terminated
if (
message.includes("connection timeout") ||
message.includes("connection terminated") ||
message.includes("timeout exceeded when trying to connect") ||
causeMessage.includes("connection terminated unexpectedly") ||
causeMessage.includes("connection timeout")
) {
return true;
}
// PostgreSQL deadlock detected - always safe to retry (one winner guaranteed)
if (code === "40P01" || message.includes("deadlock")) {
return true;
}
// PostgreSQL serialization failure
if (code === "40001") {
return true;
}
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
code === "EPIPE" ||
code === "ETIMEDOUT"
) {
return true;
}
return false;
}
// See @server/lib/dbRetry for the shared withRetry/isTransientError helpers.
// ── Lifecycle ──────────────────────────────────────────────────────────
+41 -41
View File
@@ -9,45 +9,45 @@ import {
import { canCompress } from "@server/lib/clientVersionChecks";
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
// const {
// tcpTargets,
// udpTargets,
// validHealthCheckTargets,
// browserGatewayTargets,
// remoteExitNodeSubnets
// } = await buildTargetConfigurationForNewtClient(site.siteId);
// let exitNode: ExitNode | undefined;
// if (site.exitNodeId) {
// [exitNode] = await db
// .select()
// .from(exitNodes)
// .where(eq(exitNodes.exitNodeId, site.exitNodeId))
// .limit(1);
// }
// const { peers, targets } = await buildClientConfigurationForNewtClient(
// site,
// exitNode
// );
// await sendToClient(
// newt.newtId,
// {
// type: "newt/sync",
// data: {
// proxyTargets: {
// udp: udpTargets,
// tcp: tcpTargets
// },
// healthCheckTargets: validHealthCheckTargets,
// peers: peers,
// clientTargets: targets,
// browserGatewayTargets: browserGatewayTargets,
// remoteExitNodeSubnets: remoteExitNodeSubnets
// }
// },
// {
// compress: canCompress(newt.version, "newt")
// }
// ).catch((error) => {
// logger.warn(`Error sending newt sync message:`, error);
// });
const {
tcpTargets,
udpTargets,
validHealthCheckTargets,
browserGatewayTargets,
remoteExitNodeSubnets
} = await buildTargetConfigurationForNewtClient(site.siteId);
let exitNode: ExitNode | undefined;
if (site.exitNodeId) {
[exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
.limit(1);
}
const { peers, targets } = await buildClientConfigurationForNewtClient(
site,
exitNode
);
await sendToClient(
newt.newtId,
{
type: "newt/sync",
data: {
proxyTargets: {
udp: udpTargets,
tcp: tcpTargets
},
healthCheckTargets: validHealthCheckTargets,
peers: peers,
clientTargets: targets,
browserGatewayTargets: browserGatewayTargets,
remoteExitNodeSubnets: remoteExitNodeSubnets
}
},
{
compress: canCompress(newt.version, "newt")
}
).catch((error) => {
logger.warn(`Error sending newt sync message:`, error);
});
}
+1 -1
View File
@@ -161,7 +161,7 @@ export async function buildSiteConfigurationForOlmClient(
}
if (!site.subnet) {
logger.warn(`Site ${site.siteId} has no subnet, skipping`);
logger.debug(`Site ${site.siteId} has no subnet, skipping`);
continue;
}
@@ -6,7 +6,9 @@ import logger from "@server/logger";
/**
* Handles disconnecting messages from clients to show disconnected in the ui
*/
export const handleOlmDisconnectingMessage: MessageHandler = async (context) => {
export const handleOlmDisconnectingMessage: MessageHandler = async (
context
) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
@@ -29,6 +31,6 @@ export const handleOlmDisconnectingMessage: MessageHandler = async (context) =>
})
.where(eq(clients.clientId, olm.clientId));
} catch (error) {
logger.error("Error handling disconnecting message", { error });
logger.error("Error handling client disconnecting message", error);
}
};
+5 -1
View File
@@ -268,7 +268,11 @@ export async function createSite(
let newSite: Site | undefined;
try {
if (subnet && exitNodeId) {
if (type === "wireguard" && subnet && exitNodeId) {
// Only wireguard sites actually persist the provided subnet/exitNodeId.
// Newt sites have their subnet/exit node chosen (under a lock) when the
// newt connects, so validating them here is both unnecessary and racy,
// since pickSiteDefaults does not lock the subnet it suggests.
//make sure the subnet is in the range of the exit node if provided
const [exitNode] = await db
.select()
+4
View File
@@ -3,8 +3,12 @@ import { db, orgs } from "@server/db";
import { actions, roles, roleActions } from "@server/db";
import { eq, inArray } from "drizzle-orm";
import logger from "@server/logger";
import { build } from "@server/build";
export async function ensureActions() {
if (build == "saas") {
return;
}
await db.transaction(async (trx) => {
const actionIds = Object.values(ActionsEnum);
const existingActions = await trx.select().from(actions).execute();
@@ -158,6 +158,9 @@ const tierLimits: Record<
domains: number;
remoteNodes: number;
organizations: number;
publicResources: number;
privateResources: number;
machineClients: number;
}
> = {
basic: {
@@ -165,35 +168,50 @@ const tierLimits: Record<
sites: freeLimitSet[LimitId.SITES]?.value ?? 0,
domains: freeLimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: freeLimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: freeLimitSet[LimitId.ORGANIZATIONS]?.value ?? 0
organizations: freeLimitSet[LimitId.ORGANIZATIONS]?.value ?? 0,
publicResources: freeLimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0,
privateResources: freeLimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0,
machineClients: freeLimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0
},
tier1: {
users: tier1LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier1LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier1LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier1LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier1LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0
organizations: tier1LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0,
publicResources: tier1LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0,
privateResources: tier1LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0,
machineClients: tier1LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0
},
tier2: {
users: tier2LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier2LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier2LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier2LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier2LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0
organizations: tier2LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0,
publicResources: tier2LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0,
privateResources: tier2LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0,
machineClients: tier2LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0
},
tier3: {
users: tier3LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier3LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier3LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier3LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier3LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0
organizations: tier3LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0,
publicResources: tier3LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0,
privateResources: tier3LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0,
machineClients: tier3LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0
},
enterprise: {
users: 0, // Custom for enterprise
sites: 0, // Custom for enterprise
domains: 0, // Custom for enterprise
remoteNodes: 0, // Custom for enterprise
organizations: 0 // Custom for enterprise
organizations: 0, // Custom for enterprise
publicResources: 0, // Custom for enterprise
privateResources: 0, // Custom for enterprise
machineClients: 0 // Custom for enterprise
}
};
@@ -234,6 +252,9 @@ export default function BillingPage() {
const DOMAINS = "domains";
const REMOTE_EXIT_NODES = "remoteExitNodes";
const ORGINIZATIONS = "organizations";
const PUBLIC_RESOURCES = "publicResources";
const PRIVATE_RESOURCES = "privateResources";
const MACHINE_CLIENTS = "machineClients";
// Confirmation dialog state
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
@@ -269,7 +290,13 @@ export default function BillingPage() {
setHasSubscription(
tierSub.subscription.status === "active"
);
setIsTrial(tierSub.subscription.expiresAt != null);
// expiresAt is only meaningful while the trial hasn't
// actually run out yet; a stale row with a past
// expiresAt should no longer be treated as a live trial
const expiresAt = tierSub.subscription.expiresAt;
setIsTrial(
expiresAt != null && expiresAt * 1000 > Date.now()
);
}
// Find license subscription
@@ -797,6 +824,45 @@ export default function BillingPage() {
});
}
// Check public resources
const publicResourcesUsage = getUsageValue(PUBLIC_RESOURCES);
if (
limits.publicResources > 0 &&
publicResourcesUsage > limits.publicResources
) {
violations.push({
feature: "Public Resources",
currentUsage: publicResourcesUsage,
newLimit: limits.publicResources
});
}
// Check private resources
const privateResourcesUsage = getUsageValue(PRIVATE_RESOURCES);
if (
limits.privateResources > 0 &&
privateResourcesUsage > limits.privateResources
) {
violations.push({
feature: "Private Resources",
currentUsage: privateResourcesUsage,
newLimit: limits.privateResources
});
}
// Check machine clients
const machineClientsUsage = getUsageValue(MACHINE_CLIENTS);
if (
limits.machineClients > 0 &&
machineClientsUsage > limits.machineClients
) {
violations.push({
feature: "Machine Clients",
currentUsage: machineClientsUsage,
newLimit: limits.machineClients
});
}
return violations;
};
@@ -997,21 +1063,23 @@ export default function BillingPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{/* Current Usage */}
<div className="border rounded-lg p-4">
<div className="border rounded-lg p-4 md:col-span-1">
<div className="text-sm text-muted-foreground mb-2">
{t("billingCurrentUsage") || "Current Usage"}
</div>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-semibold">
{getUserCount()}
</span>
<span className="text-lg">
{t("billingUsers") || "Users"}
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex items-baseline gap-2">
<span className="text-3xl font-semibold">
{getUserCount()}
</span>
<span className="text-lg">
{t("billingUsers") || "Users"}
</span>
</div>
{hasSubscription && getPricePerUser() > 0 && (
<div className="text-sm text-muted-foreground mt-1">
<div className="text-sm text-muted-foreground">
x ${getPricePerUser()} / month = $
{getUserCount() * getPricePerUser()} /
month
@@ -1021,11 +1089,11 @@ export default function BillingPage() {
</div>
{/* Maximum Limits */}
<div className="border rounded-lg p-4">
<div className="border rounded-lg p-4 md:col-span-3">
<div className="text-sm text-muted-foreground mb-3">
{t("billingMaximumLimits") || "Maximum Limits"}
</div>
<InfoSections cols={5}>
<InfoSections cols={8}>
<InfoSection>
<InfoSectionTitle className="flex items-center gap-1 text-xs">
{t("billingUsers") || "Users"}
@@ -1308,6 +1376,168 @@ export default function BillingPage() {
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle className="flex items-center gap-1 text-xs">
{t("billingPublicResources") ||
"Public Resources"}
</InfoSectionTitle>
<InfoSectionContent className="text-sm">
{isOverLimit(PUBLIC_RESOURCES) ? (
<Tooltip>
<TooltipTrigger className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3 text-orange-400" />
<span
className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}
>
{getLimitValue(
PUBLIC_RESOURCES
) ??
t(
"billingUnlimited"
) ??
"∞"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{t(
"billingUsageExceedsLimit",
{
current:
getUsageValue(
PUBLIC_RESOURCES
),
limit:
getLimitValue(
PUBLIC_RESOURCES
) ?? 0
}
) ||
`Current usage (${getUsageValue(PUBLIC_RESOURCES)}) exceeds limit (${getLimitValue(PUBLIC_RESOURCES)})`}
</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(
PUBLIC_RESOURCES
) ??
t("billingUnlimited") ??
"∞"}
</>
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle className="flex items-center gap-1 text-xs">
{t("billingPrivateResources") ||
"Private Resources"}
</InfoSectionTitle>
<InfoSectionContent className="text-sm">
{isOverLimit(PRIVATE_RESOURCES) ? (
<Tooltip>
<TooltipTrigger className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3 text-orange-400" />
<span
className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}
>
{getLimitValue(
PRIVATE_RESOURCES
) ??
t(
"billingUnlimited"
) ??
"∞"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{t(
"billingUsageExceedsLimit",
{
current:
getUsageValue(
PRIVATE_RESOURCES
),
limit:
getLimitValue(
PRIVATE_RESOURCES
) ?? 0
}
) ||
`Current usage (${getUsageValue(PRIVATE_RESOURCES)}) exceeds limit (${getLimitValue(PRIVATE_RESOURCES)})`}
</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(
PRIVATE_RESOURCES
) ??
t("billingUnlimited") ??
"∞"}
</>
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle className="flex items-center gap-1 text-xs">
{t("billingMachineClients") ||
"Machine Clients"}
</InfoSectionTitle>
<InfoSectionContent className="text-sm">
{isOverLimit(MACHINE_CLIENTS) ? (
<Tooltip>
<TooltipTrigger className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3 text-orange-400" />
<span
className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}
>
{getLimitValue(
MACHINE_CLIENTS
) ??
t(
"billingUnlimited"
) ??
"∞"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>
{t(
"billingUsageExceedsLimit",
{
current:
getUsageValue(
MACHINE_CLIENTS
),
limit:
getLimitValue(
MACHINE_CLIENTS
) ?? 0
}
) ||
`Current usage (${getUsageValue(MACHINE_CLIENTS)}) exceeds limit (${getLimitValue(MACHINE_CLIENTS)})`}
</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(
MACHINE_CLIENTS
) ??
t("billingUnlimited") ??
"∞"}
</>
)}
</InfoSectionContent>
</InfoSection>
</InfoSections>
</div>
</div>
@@ -1507,6 +1737,45 @@ export default function BillingPage() {
"Remote Nodes"}
</span>
</div>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
<span>
{
tierLimits[
pendingTier.tier
].publicResources
}{" "}
{t(
"billingPublicResources"
) || "Public Resources"}
</span>
</div>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
<span>
{
tierLimits[
pendingTier.tier
].privateResources
}{" "}
{t(
"billingPrivateResources"
) || "Private Resources"}
</span>
</div>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
<span>
{
tierLimits[
pendingTier.tier
].machineClients
}{" "}
{t(
"billingMachineClients"
) || "Machine Clients"}
</span>
</div>
</div>
</div>
)}