Compare commits

...

107 Commits

Author SHA1 Message Date
Owen Schwartz
3436105bec Merge pull request #2784 from fosrl/dev
Try to prevent deadlocks
2026-04-03 23:01:09 -04:00
Owen
d948d2ec33 Try to prevent deadlocks 2026-04-03 22:55:04 -04:00
Owen Schwartz
4b3375ab8e Merge pull request #2783 from fosrl/dev
Fix 1.17.0
2026-04-03 22:42:03 -04:00
Owen
6b8a3c8d77 Revert #2570
Fix #2782
2026-04-03 22:37:42 -04:00
Owen
ba9794c067 Put middleware back
Fix #2781
2026-04-03 22:16:26 -04:00
Owen Schwartz
6ce165bfd5 Merge pull request #2780 from fosrl/dev
1.17.0
2026-04-03 18:19:40 -04:00
Owen
eb4b2daaab Use the right encryption 2026-04-03 17:59:21 -04:00
Owen
8cbc8dec89 Generate address 2026-04-03 17:25:39 -04:00
Owen
e89e60d50b Encrypt the streaming data 2026-04-03 15:33:29 -04:00
Owen
c45308f234 Send to the right place 2026-04-03 15:33:29 -04:00
Owen Schwartz
40205c40c5 Merge pull request #2779 from fosrl/crowdin_dev
New Crowdin updates
2026-04-03 15:00:11 -04:00
Owen Schwartz
f3fe2dd33b New translations en-us.json (Spanish) 2026-04-03 14:58:56 -04:00
Owen Schwartz
8edcc45033 New translations en-us.json (Norwegian Bokmal) 2026-04-03 14:58:55 -04:00
Owen Schwartz
91471a4aca New translations en-us.json (Chinese Simplified) 2026-04-03 14:58:53 -04:00
Owen Schwartz
ae2c37a2f6 New translations en-us.json (Turkish) 2026-04-03 14:58:52 -04:00
Owen Schwartz
c8208f0a88 New translations en-us.json (Russian) 2026-04-03 14:58:50 -04:00
Owen Schwartz
e11dfbd29c New translations en-us.json (Portuguese) 2026-04-03 14:58:49 -04:00
Owen Schwartz
b375d20598 New translations en-us.json (Polish) 2026-04-03 14:58:48 -04:00
Owen Schwartz
c4b82c69f8 New translations en-us.json (Dutch) 2026-04-03 14:58:47 -04:00
Owen Schwartz
c9a00420a0 New translations en-us.json (Korean) 2026-04-03 14:58:45 -04:00
Owen Schwartz
36ef9cd442 New translations en-us.json (Italian) 2026-04-03 14:58:44 -04:00
Owen Schwartz
5e08779ab0 New translations en-us.json (German) 2026-04-03 14:58:42 -04:00
Owen Schwartz
16a0e1ce7b New translations en-us.json (Czech) 2026-04-03 14:58:41 -04:00
Owen Schwartz
8b03484ade New translations en-us.json (Bulgarian) 2026-04-03 14:58:39 -04:00
Owen Schwartz
9da9974adf New translations en-us.json (French) 2026-04-03 14:58:38 -04:00
Owen Schwartz
6f80cf3db2 New translations en-us.json (Spanish) 2026-04-03 13:03:44 -04:00
Owen Schwartz
76d8f44779 New translations en-us.json (Norwegian Bokmal) 2026-04-03 13:03:43 -04:00
Owen Schwartz
700c92efcb New translations en-us.json (Chinese Simplified) 2026-04-03 13:03:41 -04:00
Owen Schwartz
d17e0c9f50 New translations en-us.json (Turkish) 2026-04-03 13:03:39 -04:00
Owen Schwartz
f00b9794f5 New translations en-us.json (Russian) 2026-04-03 13:03:38 -04:00
Owen Schwartz
daff59c93f New translations en-us.json (Portuguese) 2026-04-03 13:03:36 -04:00
Owen Schwartz
aa8954366c New translations en-us.json (Polish) 2026-04-03 13:03:35 -04:00
Owen Schwartz
87464d53bd New translations en-us.json (Dutch) 2026-04-03 13:03:33 -04:00
Owen Schwartz
e04f17c9aa New translations en-us.json (Korean) 2026-04-03 13:03:32 -04:00
Owen Schwartz
b25e3499d8 New translations en-us.json (Italian) 2026-04-03 13:03:30 -04:00
Owen Schwartz
2e6f74a6f8 New translations en-us.json (German) 2026-04-03 13:03:28 -04:00
Owen Schwartz
8eee0ca5a5 New translations en-us.json (Czech) 2026-04-03 13:03:26 -04:00
Owen Schwartz
c2ebc0a0ff New translations en-us.json (Bulgarian) 2026-04-03 13:03:24 -04:00
Owen Schwartz
03c905a7af New translations en-us.json (French) 2026-04-03 13:03:22 -04:00
Owen Schwartz
035644eaf7 Merge pull request #2778 from fosrl/dev
1.17.0-s.2
2026-04-03 12:35:03 -04:00
Owen
8ce45a1acd Update casting again 2026-04-03 12:34:37 -04:00
Owen Schwartz
16e7233a3e Merge pull request #2777 from fosrl/dev
1.17.0-s.1
2026-04-03 12:19:23 -04:00
Owen
a331dd3fb4 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-04-03 12:18:33 -04:00
Owen Schwartz
e3e2938b28 Merge pull request #2761 from fosrl/crowdin_dev
New Crowdin updates
2026-04-03 12:16:31 -04:00
Owen
73e96b1b28 Type cast data 2026-04-03 12:15:39 -04:00
miloschwartz
b8194295ec fix translation syntax err 2026-04-03 11:47:54 -04:00
miloschwartz
382a46dfff fix text formatting in delete dialog 2026-04-03 11:44:56 -04:00
Owen Schwartz
1f74e1b320 Merge pull request #2776 from fosrl/dev
1.17.0-s.0
2026-04-03 11:39:35 -04:00
Owen
fee780cb81 Add siem to migration 2026-04-03 11:32:02 -04:00
Owen
5056cba85d Add siem to migration 2026-04-03 11:32:02 -04:00
miloschwartz
dab38ff82c use debug log on log stream start 2026-04-03 11:16:56 -04:00
Owen
d83fa63af5 Fix to null out the rewrite on the frontend too 2026-04-02 21:58:14 -04:00
Owen
d5837ab718 Fix to use the stored data 2026-04-02 21:57:59 -04:00
Owen Schwartz
f85cfc4c68 New translations en-us.json (Spanish) 2026-04-02 18:35:13 -04:00
Owen Schwartz
0b2aceafe0 New translations en-us.json (Norwegian Bokmal) 2026-04-02 18:35:11 -04:00
Owen Schwartz
059db34a53 New translations en-us.json (Chinese Simplified) 2026-04-02 18:35:10 -04:00
Owen Schwartz
bc1ea86b4e New translations en-us.json (Turkish) 2026-04-02 18:35:09 -04:00
Owen Schwartz
9f2ced1933 New translations en-us.json (Russian) 2026-04-02 18:35:08 -04:00
Owen Schwartz
013cff9b6e New translations en-us.json (Portuguese) 2026-04-02 18:35:06 -04:00
Owen Schwartz
aa19437031 New translations en-us.json (Polish) 2026-04-02 18:35:05 -04:00
Owen Schwartz
e848ef848b New translations en-us.json (Dutch) 2026-04-02 18:35:03 -04:00
Owen Schwartz
bb6605337f New translations en-us.json (Korean) 2026-04-02 18:35:02 -04:00
Owen Schwartz
8df8383468 New translations en-us.json (Italian) 2026-04-02 18:35:01 -04:00
Owen Schwartz
a7e9de3ac4 New translations en-us.json (German) 2026-04-02 18:34:59 -04:00
Owen Schwartz
8df41f514e New translations en-us.json (Czech) 2026-04-02 18:34:58 -04:00
Owen Schwartz
c2bf50b121 New translations en-us.json (Bulgarian) 2026-04-02 18:34:56 -04:00
Owen Schwartz
4e7dcbd7b5 New translations en-us.json (French) 2026-04-02 18:34:55 -04:00
Owen
b7ccb92236 Merge branch 'main' into dev 2026-04-02 17:39:25 -04:00
Owen Schwartz
23a151dd45 Merge pull request #2771 from LaurenceJJones/feature/systemd-install-instructions
enhance: Systemd newt instructions
2026-04-02 12:13:44 -04:00
Laurence
122079ddb2 split unix to linux and macos, show method everything other than windows, change nixos all to flake so makes sense under method 2026-04-02 17:05:50 +01:00
Owen Schwartz
1d0b0ae6ec Merge pull request #2770 from fosrl/revert-2766-feature/systemd-install-instructions
Revert "enhance: Systemd newt unit instructions"
2026-04-02 11:43:15 -04:00
Owen Schwartz
a55dd769cf Revert "enhance: Systemd newt unit instructions" 2026-04-02 11:43:01 -04:00
Owen Schwartz
f1a0bc97e3 New translations en-us.json (Spanish) 2026-04-02 11:42:52 -04:00
Owen Schwartz
a57dfd1d12 New translations en-us.json (Norwegian Bokmal) 2026-04-02 11:42:50 -04:00
Owen Schwartz
c0a8304b91 New translations en-us.json (Chinese Simplified) 2026-04-02 11:42:48 -04:00
Owen Schwartz
ab7b968e28 New translations en-us.json (Turkish) 2026-04-02 11:42:47 -04:00
Owen Schwartz
f10b40c3b0 New translations en-us.json (Russian) 2026-04-02 11:42:45 -04:00
Owen Schwartz
7878ac9c76 New translations en-us.json (Portuguese) 2026-04-02 11:42:43 -04:00
Owen Schwartz
0752951842 New translations en-us.json (Polish) 2026-04-02 11:42:41 -04:00
Owen Schwartz
06bb6636a1 New translations en-us.json (Dutch) 2026-04-02 11:42:39 -04:00
Owen Schwartz
2fdd332a31 New translations en-us.json (Korean) 2026-04-02 11:42:38 -04:00
Owen Schwartz
98b1e9546a New translations en-us.json (Italian) 2026-04-02 11:42:36 -04:00
Owen Schwartz
184aa65c6d New translations en-us.json (German) 2026-04-02 11:42:34 -04:00
Owen Schwartz
70b3a432a4 New translations en-us.json (Czech) 2026-04-02 11:42:33 -04:00
Owen Schwartz
fb4fc75bd8 New translations en-us.json (Bulgarian) 2026-04-02 11:42:31 -04:00
Owen Schwartz
0479ed9e7f New translations en-us.json (French) 2026-04-02 11:42:29 -04:00
Owen Schwartz
1dc3409135 Merge pull request #2766 from LaurenceJJones/feature/systemd-install-instructions
enhance: Systemd newt unit instructions
2026-04-02 11:40:50 -04:00
Laurence
1bb89fce26 enhance: Systemd newt unit
Add a systemd unit option directly from dashboard to prevent copy and paste mistakes
2026-04-02 12:21:53 +01:00
Owen Schwartz
8f3fbb94d2 New translations en-us.json (Spanish) 2026-04-01 09:58:52 -07:00
Owen Schwartz
e8c35bec1c New translations en-us.json (Norwegian Bokmal) 2026-04-01 09:58:50 -07:00
Owen Schwartz
728e7252eb New translations en-us.json (Chinese Simplified) 2026-04-01 09:58:49 -07:00
Owen Schwartz
1218507f7d New translations en-us.json (Turkish) 2026-04-01 09:58:47 -07:00
Owen Schwartz
a2dff0a35d New translations en-us.json (Russian) 2026-04-01 09:58:46 -07:00
Owen Schwartz
f411180908 New translations en-us.json (Portuguese) 2026-04-01 09:58:44 -07:00
Owen Schwartz
231a19b679 New translations en-us.json (Polish) 2026-04-01 09:58:43 -07:00
Owen Schwartz
58a87a986a New translations en-us.json (Dutch) 2026-04-01 09:58:41 -07:00
Owen Schwartz
61a78ef352 New translations en-us.json (Korean) 2026-04-01 09:58:39 -07:00
Owen Schwartz
e28e5ebb4e New translations en-us.json (Italian) 2026-04-01 09:58:38 -07:00
Owen Schwartz
19cef8c453 New translations en-us.json (German) 2026-04-01 09:58:36 -07:00
Owen Schwartz
1290d6cd5c New translations en-us.json (Czech) 2026-04-01 09:58:35 -07:00
Owen Schwartz
ad301074db New translations en-us.json (Bulgarian) 2026-04-01 09:58:33 -07:00
Owen Schwartz
30a756d254 New translations en-us.json (French) 2026-04-01 09:58:32 -07:00
Owen
363c13c387 Impvove communication 2026-04-01 09:53:49 -07:00
Owen
08e4afaef0 Update hp log message 2026-03-31 17:06:56 -07:00
Owen
69aa6e2d1d Prevent increase in writes on reconnect 2026-03-31 17:00:06 -07:00
Owen
547865e0da Mark targets unhealthy when site is down
Fix #2675
Fix #2700
Fix #1742
2026-03-31 16:24:53 -07:00
Owen
3a9e79e6d5 Filter only newt sites on private resources 2026-03-31 16:17:17 -07:00
45 changed files with 630 additions and 268 deletions

View File

@@ -60,7 +60,7 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
| <img width=500 /> | Description |
|-----------------|--------------|
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing - no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |

View File

@@ -86,6 +86,8 @@ entryPoints:
http:
tls:
certResolver: "letsencrypt"
middlewares:
- crowdsec@file
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Ключът за осигуряване е актуализиран",
"provisioningKeysUpdatedDescription": "Вашите промени бяха запазени.",
"provisioningKeysBannerTitle": "Ключове за осигуряване на сайта",
"provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте с Newt конектора за автоматично създаване на сайтове при първото стартиране няма нужда от създаване на отделни идентификационни данни за всеки сайт.",
"provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте със съединителя Newt за автоматично създаване на сайтове при първоначално стартиране - не е необходимо да се създават отделни идентификационни данни за всеки сайт.",
"provisioningKeysBannerButtonText": "Научете повече",
"pendingSitesBannerTitle": "Чакащи сайтове",
"pendingSitesBannerDescription": "Сайтовете, които се свързват чрез ключ за осигуряване, се появяват тук за преглед. Одобрете всеки сайт, преди да стане активен и да получи достъп до вашите ресурси.",
"pendingSitesBannerDescription": "Сайтовете, които се свързват с ключ за осигуряване, ще се появят тук за преглед.",
"pendingSitesBannerButtonText": "Научете повече",
"apiKeysSettings": "Настройки на {apiKeyName}",
"userTitle": "Управление на всички потребители",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
"targetErrorNoSite": "Няма избран сайт",
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
"targetTargetsCleared": "Мишените са премахнати",
"targetTargetsClearedDescription": "Всички цели са били премахнати от този ресурс",
"targetCreated": "Целта е създадена",
"targetCreatedDescription": "Целта беше успешно създадена",
"targetErrorCreate": "Неуспешно създаване на целта",
@@ -2346,7 +2348,7 @@
"description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка."
}
},
"personalUseOnly": "Само за лична употреба (безплатен лиценз без плащане)",
"personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)",
"buttons": {
"continueToCheckout": "Продължете към плащане"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Машинни клиенти",
"install": "Инсталирай",
"run": "Изпълни",
"envFile": "Файл за среда",
"serviceFile": "Файл за услуга",
"enableAndStart": "Активиране и стартиране",
"clientNameDescription": "Показваното име на клиента, което може да се промени по-късно.",
"clientAddress": "Клиентски адрес (Разширено)",
"setupFailedToFetchSubnet": "Неуспешно извличане на подмрежа по подразбиране",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Без удостоверяване",
"httpDestAuthNoneDescription": "Изпращане на заявки без заглавие за удостоверяване.",
"httpDestAuthBearerTitle": "Bearer Токен",
"httpDestAuthBearerDescription": "Добавя заглавие за удостоверяване Bearer <token> към всяка заявка.",
"httpDestAuthBearerDescription": "Добавя заглавие Authorization: Bearer '<token>' към всяка заявка.",
"httpDestAuthBearerPlaceholder": "Вашият API ключ или токен",
"httpDestAuthBasicTitle": "Основно удостоверяване",
"httpDestAuthBasicDescription": "Добавя заглавие за удостоверяване Basic <credentials> към всяка заявка. Осигурете идентификационни данни като потребителско име:парола.",
"httpDestAuthBasicDescription": "Добавя заглавие Authorization: Basic '<credentials>'. Осигурете идентификационни данни като потребителско име:парола.",
"httpDestAuthBasicPlaceholder": "потребителско име:парола",
"httpDestAuthCustomTitle": "Персонализирано заглавие",
"httpDestAuthCustomDescription": "Посочете персонализирано име и стойност на заглавието за удостоверяване (например X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Zajišťovací klíč byl aktualizován",
"provisioningKeysUpdatedDescription": "Vaše změny byly uloženy.",
"provisioningKeysBannerTitle": "Klíče pro poskytování webu",
"provisioningKeysBannerDescription": "Vygenerujte konfigurační klíč a používejte jej pomocí nového konektoru k automatickému vytváření stránek při prvním startu není třeba nastavovat samostatné přihlašovací údaje pro každý web.",
"provisioningKeysBannerDescription": "Vygenerujte klíč pro zřízení a použijte ho s Newt konektorem k automatickému vytvoření stránek při prvním spuštění není potřeba nastavit samostatné přihlašovací údaje pro každou stránku.",
"provisioningKeysBannerButtonText": "Zjistit více",
"pendingSitesBannerTitle": "Nevyřízené weby",
"pendingSitesBannerDescription": "Zde se zobrazují stránky, které se připojují pomocí doplňovacího klíče. Schválte každý web předtím, než bude aktivní, a získejte přístup k vašim zdrojům.",
"pendingSitesBannerDescription": "Stránky, které se připojují pomocí klíče pro zřízení, se zde objeví ke kontrole.",
"pendingSitesBannerButtonText": "Zjistit více",
"apiKeysSettings": "Nastavení {apiKeyName}",
"userTitle": "Spravovat všechny uživatele",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Zadejte platné číslo portu",
"targetErrorNoSite": "Není vybrán žádný web",
"targetErrorNoSiteDescription": "Vyberte prosím web pro cíl",
"targetTargetsCleared": "Cíle vymazány",
"targetTargetsClearedDescription": "Všechny cíle byly odstraněny z tohoto zdroje",
"targetCreated": "Cíl byl vytvořen",
"targetCreatedDescription": "Cíl byl úspěšně vytvořen",
"targetErrorCreate": "Nepodařilo se vytvořit cíl",
@@ -2346,7 +2348,7 @@
"description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory."
}
},
"personalUseOnly": "Pouze osobní použití (bezplatná licence bez platby)",
"personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)",
"buttons": {
"continueToCheckout": "Pokračovat do pokladny"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Strojoví klienti",
"install": "Instalovat",
"run": "Spustit",
"envFile": "Konfigurační soubor prostředí",
"serviceFile": "Služební soubor",
"enableAndStart": "Povolit a spustit",
"clientNameDescription": "Zobrazované jméno klienta, které lze později změnit.",
"clientAddress": "Adresa klienta (Rozšířeno)",
"setupFailedToFetchSubnet": "Nepodařilo se načíst výchozí podsíť",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Žádné ověření",
"httpDestAuthNoneDescription": "Odešle žádosti bez záhlaví autorizace.",
"httpDestAuthBearerTitle": "Token na doručitele",
"httpDestAuthBearerDescription": "Přidá autorizaci: Hlavička Bearer <token> ke každému požadavku.",
"httpDestAuthBearerDescription": "Přidává hlavičku Authorization: Bearer '<token>' k každému požadavku.",
"httpDestAuthBearerPlaceholder": "Váš API klíč nebo token",
"httpDestAuthBasicTitle": "Základní ověření",
"httpDestAuthBasicDescription": "Přidá autorizaci: Základní <credentials> hlavička. Poskytněte přihlašovací údaje jako uživatelské jméno:password.",
"httpDestAuthBasicDescription": "Přidává hlavičku Authorization: Basic '<credentials>'. Poskytněte přihlašovací údaje ve formátu uživatelské jméno:heslo.",
"httpDestAuthBasicPlaceholder": "uživatelské jméno:heslo",
"httpDestAuthCustomTitle": "Vlastní záhlaví",
"httpDestAuthCustomDescription": "Zadejte název a hodnotu vlastního HTTP hlavičky pro ověření (např. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert",
"provisioningKeysUpdatedDescription": "Ihre Änderungen wurden gespeichert.",
"provisioningKeysBannerTitle": "Website-Bereitstellungsschlüssel",
"provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Konnektor, um beim ersten Start automatisch Sites zu erstellen keine Notwendigkeit, separate Anmeldeinformationen für jede Seite einzurichten.",
"provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.",
"provisioningKeysBannerButtonText": "Mehr erfahren",
"pendingSitesBannerTitle": "Ausstehende Seiten",
"pendingSitesBannerDescription": "Sites, die sich mit einem Bereitstellungsschlüssel verbinden, erscheinen hier zur Überprüfung. Bestätigen Sie jede Site, bevor sie aktiv wird und erhalten Zugriff auf Ihre Ressourcen.",
"pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.",
"pendingSitesBannerButtonText": "Mehr erfahren",
"apiKeysSettings": "{apiKeyName} Einstellungen",
"userTitle": "Alle Benutzer verwalten",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein",
"targetErrorNoSite": "Kein Standort ausgewählt",
"targetErrorNoSiteDescription": "Bitte wähle einen Standort für das Ziel aus",
"targetTargetsCleared": "Ziele gelöscht",
"targetTargetsClearedDescription": "Alle Ziele wurden aus dieser Ressource entfernt",
"targetCreated": "Ziel erstellt",
"targetCreatedDescription": "Ziel wurde erfolgreich erstellt",
"targetErrorCreate": "Fehler beim Erstellen des Ziels",
@@ -2346,7 +2348,7 @@
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung."
}
},
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz keine Kasse)",
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)",
"buttons": {
"continueToCheckout": "Weiter zur Kasse"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Maschinen-Clients",
"install": "Installieren",
"run": "Ausführen",
"envFile": "Umgebungsdatei",
"serviceFile": "Servicedatei",
"enableAndStart": "Aktivieren und Starten",
"clientNameDescription": "Der Anzeigename des Clients, der später geändert werden kann.",
"clientAddress": "Clientadresse (Erweitert)",
"setupFailedToFetchSubnet": "Fehler beim Abrufen des Standard-Subnetzes",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Keine Authentifizierung",
"httpDestAuthNoneDescription": "Sendet Anfragen ohne Autorisierungs-Header.",
"httpDestAuthBearerTitle": "Bären-Token",
"httpDestAuthBearerDescription": "Fügt eine Berechtigung hinzu: Bearer <token> Header zu jeder Anfrage.",
"httpDestAuthBearerDescription": "Fügt jedem Anfrage-Header eine \"Authorization: Bearer '<token>'\" hinzu.",
"httpDestAuthBearerPlaceholder": "Ihr API-Schlüssel oder Token",
"httpDestAuthBasicTitle": "Einfacher Auth",
"httpDestAuthBasicDescription": "Fügt eine Autorisierung hinzu: Basic <credentials> Kopfzeile hinzu. Geben Sie Anmeldedaten als Benutzername:password an.",
"httpDestAuthBasicDescription": "Fügt einen \"Authorization: Basic '<credentials>'\"-Header hinzu. Geben Sie die Anmeldedaten als Benutzername:Passwort an.",
"httpDestAuthBasicPlaceholder": "benutzername:password",
"httpDestAuthCustomTitle": "Eigene Kopfzeile",
"httpDestAuthCustomDescription": "Geben Sie einen eigenen HTTP-Header-Namen und einen Wert für die Authentifizierung an (z.B. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup no need to set up separate credentials for each site.",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup - no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} Settings",
"userTitle": "Manage All Users",
@@ -2348,7 +2348,7 @@
"description": "Enterprise features, 50 users, 50 sites, and priority support."
}
},
"personalUseOnly": "Personal use only (free license no checkout)",
"personalUseOnly": "Personal use only (free license - no checkout)",
"buttons": {
"continueToCheckout": "Continue to Checkout"
},
@@ -2609,6 +2609,9 @@
"machineClients": "Machine Clients",
"install": "Install",
"run": "Run",
"envFile": "Environment File",
"serviceFile": "Service File",
"enableAndStart": "Enable and Start",
"clientNameDescription": "The display name of the client that can be changed later.",
"clientAddress": "Client Address (Advanced)",
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
@@ -2847,10 +2850,10 @@
"httpDestAuthNoneTitle": "No Authentication",
"httpDestAuthNoneDescription": "Sends requests without an Authorization header.",
"httpDestAuthBearerTitle": "Bearer Token",
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer <token> header to each request.",
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer '<token>' header to each request.",
"httpDestAuthBearerPlaceholder": "Your API key or token",
"httpDestAuthBasicTitle": "Basic Auth",
"httpDestAuthBasicDescription": "Adds an Authorization: Basic <credentials> header. Provide credentials as username:password.",
"httpDestAuthBasicDescription": "Adds an Authorization: Basic '<credentials>' header. Provide credentials as username:password.",
"httpDestAuthBasicPlaceholder": "username:password",
"httpDestAuthCustomTitle": "Custom Header",
"httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Clave de aprovisionamiento actualizada",
"provisioningKeysUpdatedDescription": "Sus cambios han sido guardados.",
"provisioningKeysBannerTitle": "Claves de aprovisionamiento del sitio",
"provisioningKeysBannerDescription": "Generar una clave de aprovisionamiento y usarla con el conector Newt para crear automáticamente sitios en el primer inicio no es necesario configurar credenciales separadas para cada sitio.",
"provisioningKeysBannerDescription": "Genere una clave de aprovisionamiento y utilícela con el conector Newt para crear automáticamente sitios en el primer inicio: no es necesario configurar credenciales separadas para cada sitio.",
"provisioningKeysBannerButtonText": "Saber más",
"pendingSitesBannerTitle": "Sitios pendientes",
"pendingSitesBannerDescription": "Los sitios que se conectan usando una clave de aprovisionamiento aparecen aquí para su revisión. Aprobar cada sitio antes de que se active y obtenga acceso a sus recursos.",
"pendingSitesBannerDescription": "Los sitios que se conectan utilizando una clave de aprovisionamiento aparecen aquí para su revisión.",
"pendingSitesBannerButtonText": "Saber más",
"apiKeysSettings": "Ajustes {apiKeyName}",
"userTitle": "Administrar todos los usuarios",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido",
"targetErrorNoSite": "Ningún sitio seleccionado",
"targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo",
"targetTargetsCleared": "Objetivos eliminados",
"targetTargetsClearedDescription": "Todos los objetivos han sido eliminados de este recurso",
"targetCreated": "Objetivo creado",
"targetCreatedDescription": "El objetivo se ha creado correctamente",
"targetErrorCreate": "Error al crear el objetivo",
@@ -2346,7 +2348,7 @@
"description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario."
}
},
"personalUseOnly": "Solo uso personal (licencia gratuita, sin pago)",
"personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)",
"buttons": {
"continueToCheckout": "Continuar con el pago"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Clientes de la máquina",
"install": "Instalar",
"run": "Ejecutar",
"envFile": "Archivo de Entorno",
"serviceFile": "Archivo de Servicio",
"enableAndStart": "Habilitar y empezar",
"clientNameDescription": "El nombre mostrado del cliente que se puede cambiar más adelante.",
"clientAddress": "Dirección del cliente (Avanzado)",
"setupFailedToFetchSubnet": "No se pudo obtener la subred por defecto",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Sin autenticación",
"httpDestAuthNoneDescription": "Envía solicitudes sin un encabezado de autorización.",
"httpDestAuthBearerTitle": "Tóken de portador",
"httpDestAuthBearerDescription": "Añade una autorización: portador <token> encabezado a cada solicitud.",
"httpDestAuthBearerDescription": "Añade un encabezado Authorization: Bearer '<token>' a cada solicitud.",
"httpDestAuthBearerPlaceholder": "Tu clave o token API",
"httpDestAuthBasicTitle": "Auth Básica",
"httpDestAuthBasicDescription": "Añade una Autorización: encabezado básico <credentials> . Proporcione credenciales como nombre de usuario: contraseña.",
"httpDestAuthBasicDescription": "Añade un encabezado Authorization: Basic '<credenciales>'. Proporcione las credenciales como nombredeusuario:contraseña.",
"httpDestAuthBasicPlaceholder": "usuario:contraseña",
"httpDestAuthCustomTitle": "Cabecera personalizada",
"httpDestAuthCustomDescription": "Especifique un nombre de cabecera HTTP personalizado y un valor para la autenticación (por ejemplo, X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Clé de provisioning mise à jour",
"provisioningKeysUpdatedDescription": "Vos modifications ont été enregistrées.",
"provisioningKeysBannerTitle": "Clés de provisioning du site",
"provisioningKeysBannerDescription": "Générez une clé de provisioning et utilisez-la avec le connecteur Newt pour créer automatiquement des sites au premier démarrage — pas besoin de configurer des identifiants distincts pour chaque site.",
"provisioningKeysBannerDescription": "Générez une clé de provisionnement et utilisez-la avec le connecteur Newt pour créer automatiquement des sites lors du premier démarrage - sans besoin de configurer des identifiants séparés pour chaque site.",
"provisioningKeysBannerButtonText": "En savoir plus",
"pendingSitesBannerTitle": "Sites en attente",
"pendingSitesBannerDescription": "Les sites qui se connectent à l'aide d'une clé de provisioning apparaissent ici pour être revus. Approuver chaque site avant qu'il ne devienne actif et qu'il accède à vos ressources.",
"pendingSitesBannerDescription": "Les sites qui se connectent en utilisant une clé de provisionnement apparaissent ici pour révision.",
"pendingSitesBannerButtonText": "En savoir plus",
"apiKeysSettings": "Paramètres de {apiKeyName}",
"userTitle": "Gérer tous les utilisateurs",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide",
"targetErrorNoSite": "Aucun site sélectionné",
"targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible",
"targetTargetsCleared": "Cibles effacées",
"targetTargetsClearedDescription": "Toutes les cibles ont été retirées de cette ressource",
"targetCreated": "Cible créée",
"targetCreatedDescription": "La cible a été créée avec succès",
"targetErrorCreate": "Impossible de créer la cible",
@@ -2346,7 +2348,7 @@
"description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire."
}
},
"personalUseOnly": "Utilisation personnelle uniquement (licence gratuite — sans checkout)",
"personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)",
"buttons": {
"continueToCheckout": "Continuer vers le paiement"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Clients Machines",
"install": "Installer",
"run": "Exécuter",
"envFile": "Fichier Environnement",
"serviceFile": "Fichier de Service",
"enableAndStart": "Activer et Démarrer",
"clientNameDescription": "Le nom d'affichage du client qui peut être modifié plus tard.",
"clientAddress": "Adresse du client (Avancé)",
"setupFailedToFetchSubnet": "Impossible de récupérer le sous-réseau par défaut",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Aucune authentification",
"httpDestAuthNoneDescription": "Envoie des requêtes sans en-tête d'autorisation.",
"httpDestAuthBearerTitle": "Jeton de Porteur",
"httpDestAuthBearerDescription": "Ajoute un en-tête Authorization: Bearer <token> à chaque requête.",
"httpDestAuthBearerDescription": "Ajoute un en-tête Authorization: Bearer '<token>' à chaque requête.",
"httpDestAuthBearerPlaceholder": "Votre clé API ou votre jeton",
"httpDestAuthBasicTitle": "Authentification basique",
"httpDestAuthBasicDescription": "Ajoute une autorisation : en-tête de base <credentials> . Fournissez des informations d'identification comme nom d'utilisateur:mot de passe.",
"httpDestAuthBasicDescription": "Ajoute un en-tête Authorization: Basic '<credentials>'. Fournissez les identifiants sous la forme nom d'utilisateur:mot de passe.",
"httpDestAuthBasicPlaceholder": "nom d'utilisateur:mot de passe",
"httpDestAuthCustomTitle": "En-tête personnalisé",
"httpDestAuthCustomDescription": "Spécifiez un nom d'en-tête HTTP personnalisé et une valeur pour l'authentification (par exemple X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Chiave di accantonamento aggiornata",
"provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.",
"provisioningKeysBannerTitle": "Chiavi Di Provvedimento Sito",
"provisioningKeysBannerDescription": "Generare una chiave di provisioning e usarla con il connettore Newt per creare automaticamente siti al primo avvio non è necessario impostare credenziali separate per ogni sito.",
"provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.",
"provisioningKeysBannerButtonText": "Scopri di più",
"pendingSitesBannerTitle": "Siti In Attesa",
"pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning appaiono qui per la revisione. Approva ogni sito prima che diventi attivo e ottenga l'accesso alle tue risorse.",
"pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning vengono visualizzati qui per la revisione.",
"pendingSitesBannerButtonText": "Scopri di più",
"apiKeysSettings": "Impostazioni {apiKeyName}",
"userTitle": "Gestisci Tutti Gli Utenti",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Inserisci un numero di porta valido",
"targetErrorNoSite": "Nessun sito selezionato",
"targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo",
"targetTargetsCleared": "Obiettivi cancellati",
"targetTargetsClearedDescription": "Tutti gli obiettivi sono stati rimossi da questa risorsa",
"targetCreated": "Destinazione creata",
"targetCreatedDescription": "L'obiettivo è stato creato con successo",
"targetErrorCreate": "Impossibile creare l'obiettivo",
@@ -2346,7 +2348,7 @@
"description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario."
}
},
"personalUseOnly": "Solo uso personale (licenza gratuita nessun checkout)",
"personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)",
"buttons": {
"continueToCheckout": "Continua al Checkout"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Machine Clients",
"install": "Installa",
"run": "Esegui",
"envFile": "File di ambiente",
"serviceFile": "File di servizio",
"enableAndStart": "Abilita e avvia",
"clientNameDescription": "Il nome visualizzato del client che può essere modificato in seguito.",
"clientAddress": "Indirizzo Client (Avanzato)",
"setupFailedToFetchSubnet": "Recupero della sottorete predefinita non riuscito",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Nessuna Autenticazione",
"httpDestAuthNoneDescription": "Invia richieste senza intestazione autorizzazione.",
"httpDestAuthBearerTitle": "Token Del Portatore",
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Autorizzazione: Bearer <token> ad ogni richiesta.",
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Authorization: Bearer '<token>' a ogni richiesta.",
"httpDestAuthBearerPlaceholder": "La tua chiave API o token",
"httpDestAuthBasicTitle": "Autenticazione Base",
"httpDestAuthBasicDescription": "Aggiunge un'autorizzazione: intestazione di base <credentials> . Fornisce le credenziali come username:password.",
"httpDestAuthBasicDescription": "Aggiunge un'intestazione Authorization: Basic '<credentials>'. Fornire le credenziali come username:password.",
"httpDestAuthBasicPlaceholder": "username:password",
"httpDestAuthCustomTitle": "Intestazione Personalizzata",
"httpDestAuthCustomDescription": "Specifica un nome e un valore di intestazione HTTP personalizzati per l'autenticazione (ad esempio X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "프로비저닝 키가 업데이트되었습니다",
"provisioningKeysUpdatedDescription": "변경 사항이 저장되었습니다.",
"provisioningKeysBannerTitle": "사이트 프로비저닝 키",
"provisioningKeysBannerDescription": "프로비저닝 키를 생성하 Newt 커넥터와 함께 사용실행자동으로 사이트를 생성하세요 — 각 사이트마다 별도의 인증을 설정할 필요 없습니다.",
"provisioningKeysBannerDescription": "프로비저닝 키를 생성하 Newt 커넥터와 함께 사용하여시작사이트를 자동 생성 - 각 사이트에 대한 별도 자격 증명이 필요 없습니다.",
"provisioningKeysBannerButtonText": "자세히 알아보기",
"pendingSitesBannerTitle": "대기중인 사이트",
"pendingSitesBannerDescription": "프로비저닝 키를 사용하여 연결하는 사이트 검토 대기 중입니다. 사이트가 활성화되어 리소스에 액세스하기 전에 각 사이트를 승인하세요.",
"pendingSitesBannerDescription": "프로비저닝 키를 사용하여 연결 사이트 검토를 위해 여기에 표시됩니다.",
"pendingSitesBannerButtonText": "자세히 알아보기",
"apiKeysSettings": "{apiKeyName} 설정",
"userTitle": "모든 사용자 관리",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
"targetErrorNoSite": "선택된 사이트 없음",
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
"targetTargetsCleared": "대상이 제거됨",
"targetTargetsClearedDescription": "이 리소스에서 모든 대상이 제거되었습니다",
"targetCreated": "대상 생성",
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
"targetErrorCreate": "대상 생성 실패",
@@ -2346,7 +2348,7 @@
"description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원."
}
},
"personalUseOnly": "개인 사용 전용 (무료 라이— 체크아웃 없음)",
"personalUseOnly": "개인용으로만 사용 (무료 라이- 결제 없음)",
"buttons": {
"continueToCheckout": "결제로 진행"
},
@@ -2607,6 +2609,9 @@
"machineClients": "기계 클라이언트",
"install": "설치",
"run": "실행",
"envFile": "환경 파일",
"serviceFile": "서비스 파일",
"enableAndStart": "활성화 및 시작",
"clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.",
"clientAddress": "클라이언트 주소(고급)",
"setupFailedToFetchSubnet": "기본값 로드 실패",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "인증 없음",
"httpDestAuthNoneDescription": "Authorization 헤더 없이 요청을 보냅니다.",
"httpDestAuthBearerTitle": "Bearer 토큰",
"httpDestAuthBearerDescription": "모든 요청에 Authorization: Bearer <token> 헤더를 추가합니다.",
"httpDestAuthBearerDescription": " 요청에 Authorization: Bearer '<token>' 헤더를 추가합니다.",
"httpDestAuthBearerPlaceholder": "API 키 또는 토큰",
"httpDestAuthBasicTitle": "기본 인증",
"httpDestAuthBasicDescription": "Authorization: Basic <credentials> 헤더를 추가합니다. 자격 증명은 username:password 형식으로 제공하세요.",
"httpDestAuthBasicDescription": "Authorization: Basic '<credentials>' 헤더를 추가합니다. 자격 증명은 사용자 이름:비밀번호로 제공합니다.",
"httpDestAuthBasicPlaceholder": "사용자 이름:비밀번호",
"httpDestAuthCustomTitle": "사용자 정의 헤더",
"httpDestAuthCustomDescription": "인증을 위한 사용자 정의 HTTP 헤더 이름 및 값을 지정하세요 (예: X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Foreslå nøkkel oppdatert",
"provisioningKeysUpdatedDescription": "Dine endringer er lagret.",
"provisioningKeysBannerTitle": "Sidens bestemmende nøkler",
"provisioningKeysBannerDescription": "Generer en foreløpig nøkkel og bruk den med Nyhetskontakten for å automatisk opprette sider ved første oppstart — trenger ikke å sette opp separat innloggingsinformasjon for hver side.",
"provisioningKeysBannerDescription": "Generer en provisjonsnøkkel og bruk den med Newt-kontakten for automatisk opprettelse av nettsteder ved første oppstart - ingen behov for å sette opp separate legitimasjoner for hvert nettsted.",
"provisioningKeysBannerButtonText": "Lær mer",
"pendingSitesBannerTitle": "Ventende nettsteder",
"pendingSitesBannerDescription": "Nettsteder som kobler deg til ved hjelp av en bestemmelsestekst, vises her for gjennomgang. Godkjenn hvert nettsted før det blir aktivt og får tilgang til ressursene dine.",
"pendingSitesBannerDescription": "Nettsteder som kobler seg til ved bruk av en provisjonsnøkkel vises her for vurdering.",
"pendingSitesBannerButtonText": "Lær mer",
"apiKeysSettings": "{apiKeyName} Innstillinger",
"userTitle": "Administrer alle brukere",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer",
"targetErrorNoSite": "Ingen nettsted valgt",
"targetErrorNoSiteDescription": "Velg et nettsted for målet",
"targetTargetsCleared": "Mål ryddet",
"targetTargetsClearedDescription": "Alle mål har blitt fjernet fra denne ressursen",
"targetCreated": "Mål opprettet",
"targetCreatedDescription": "Målet har blitt opprettet",
"targetErrorCreate": "Kunne ikke opprette målet",
@@ -2346,7 +2348,7 @@
"description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte."
}
},
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)",
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)",
"buttons": {
"continueToCheckout": "Fortsett til kassen"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Maskinklienter",
"install": "Installer",
"run": "Kjør",
"envFile": "Miljøfil",
"serviceFile": "Tjenestefil",
"enableAndStart": "Aktiver og start",
"clientNameDescription": "Visningsnavnet til klienten som kan endres senere.",
"clientAddress": "Klientadresse (avansert)",
"setupFailedToFetchSubnet": "Kunne ikke hente standard undernett",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Ingen godkjenning",
"httpDestAuthNoneDescription": "Sender forespørsler uten autorisasjonsoverskrift.",
"httpDestAuthBearerTitle": "Bærer Symbol",
"httpDestAuthBearerDescription": "Legger til en autorisasjon: Bearer <token> header til hver forespørsel.",
"httpDestAuthBearerDescription": "Legger til en Autorisasjon: Bearer '<token>' header til hver forespørsel.",
"httpDestAuthBearerPlaceholder": "Din API-nøkkel eller token",
"httpDestAuthBasicTitle": "Standard Auth",
"httpDestAuthBasicDescription": "Legger til en godkjenning: Grunnleggende <credentials> overskrift. Angi legitimasjon som brukernavn:passord.",
"httpDestAuthBasicDescription": "Legger til en Autorisasjon: Basic '<credentials>' header. Gi legitimasjon som brukernavn:passord.",
"httpDestAuthBasicPlaceholder": "brukernavn:passord",
"httpDestAuthCustomTitle": "Egendefinert topptekst",
"httpDestAuthCustomDescription": "Angi et egendefinert HTTP headers navn og verdi for autentisering (f.eks X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Provisie sleutel bijgewerkt",
"provisioningKeysUpdatedDescription": "Uw wijzigingen zijn opgeslagen.",
"provisioningKeysBannerTitle": "Bewerkingssleutels voor websites",
"provisioningKeysBannerDescription": "Genereer een provisioning-sleutel en gebruik deze met de Newt-connector om automatisch sites aan te maken bij het opstarten van de eerste opstart- het is niet nodig om afzonderlijke inloggegevens in te stellen voor elke site.",
"provisioningKeysBannerDescription": "Genereer een inrichtingssleutel en gebruik deze met de Newt-connector om automatisch sites te maken bij de eerste opstart - er is geen behoefte om aparte inloggegevens voor elke site in te stellen.",
"provisioningKeysBannerButtonText": "Meer informatie",
"pendingSitesBannerTitle": "Openstaande sites",
"pendingSitesBannerDescription": "Sites die met elkaar verbinden met behulp van een provisioning-sleutel verschijnen hier voor beoordeling. Accepteer elke site voordat deze actief wordt en krijgt toegang tot uw bronnen.",
"pendingSitesBannerDescription": "Sites die verbinding maken met een inrichtingssleutel verschijnen hier voor beoordeling.",
"pendingSitesBannerButtonText": "Meer informatie",
"apiKeysSettings": "{apiKeyName} instellingen",
"userTitle": "Alle gebruikers beheren",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Voer een geldig poortnummer in",
"targetErrorNoSite": "Geen site geselecteerd",
"targetErrorNoSiteDescription": "Selecteer een site voor het doel",
"targetTargetsCleared": "Doelen gewist",
"targetTargetsClearedDescription": "Alle doelen zijn verwijderd van deze bron",
"targetCreated": "Doel aangemaakt",
"targetCreatedDescription": "Doel is succesvol aangemaakt",
"targetErrorCreate": "Kan doel niet aanmaken",
@@ -2346,7 +2348,7 @@
"description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning."
}
},
"personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)",
"personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)",
"buttons": {
"continueToCheckout": "Doorgaan naar afrekenen"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Machine Clienten",
"install": "Installeren",
"run": "Uitvoeren",
"envFile": "Omgevingsbestand",
"serviceFile": "Servicebestand",
"enableAndStart": "Inschakelen en Starten",
"clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.",
"clientAddress": "Klant adres (Geavanceerd)",
"setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Geen authenticatie",
"httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.",
"httpDestAuthBearerTitle": "Betere Token",
"httpDestAuthBearerDescription": "Voegt een machtiging toe: Drager <token> header aan elke aanvraag.",
"httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '<token>' header toe aan elk verzoek.",
"httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token",
"httpDestAuthBasicTitle": "Basis authenticatie",
"httpDestAuthBasicDescription": "Voegt een Authorizatie toe: Basis <credentials> kop. Geef inloggegevens op als gebruikersnaam:wachtwoord.",
"httpDestAuthBasicDescription": "Voegt een Authorization: Basic '<credentials>' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.",
"httpDestAuthBasicPlaceholder": "Gebruikersnaam:wachtwoord",
"httpDestAuthCustomTitle": "Aangepaste koptekst",
"httpDestAuthCustomDescription": "Specificeer een aangepaste HTTP header naam en waarde voor authenticatie (bijv. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Klucz zaopatrzenia zaktualizowany",
"provisioningKeysUpdatedDescription": "Twoje zmiany zostały zapisane.",
"provisioningKeysBannerTitle": "Klucze Zaopatrzenia witryny",
"provisioningKeysBannerDescription": "Wygeneruj klucz tworzenia rezerw i użyj go z konektorem Newt do automatycznego tworzenia witryn przy pierwszym uruchomieniu nie ma potrzeby ustawiania oddzielnych poświadczeń dla każdej witryny.",
"provisioningKeysBannerDescription": "Wygeneruj klucz provisioning i użyj go z konektorem Newt do automatycznego tworzenia witryn przy pierwszym uruchomieniu - nie ma potrzeby konfigurowania oddzielnych poświadczeń dla każdej witryny.",
"provisioningKeysBannerButtonText": "Dowiedz się więcej",
"pendingSitesBannerTitle": "Witryny oczekujące",
"pendingSitesBannerDescription": "Witryny, które łączą się przy użyciu klucza zaopatrzenia, pojawiają się tutaj, aby przejrzeć. Zatwierdź każdą witrynę, zanim stanie się aktywna i uzyska dostęp do twoich zasobów.",
"pendingSitesBannerDescription": "Witryny, które łączą się za pomocą klucza provisioning, pojawią się tutaj do przeglądu.",
"pendingSitesBannerButtonText": "Dowiedz się więcej",
"apiKeysSettings": "Ustawienia {apiKeyName}",
"userTitle": "Zarządzaj wszystkimi użytkownikami",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu",
"targetErrorNoSite": "Nie wybrano witryny",
"targetErrorNoSiteDescription": "Wybierz witrynę docelową",
"targetTargetsCleared": "Cele wyczyszczone",
"targetTargetsClearedDescription": "Wszystkie cele zostały usunięte z tego zasobu",
"targetCreated": "Cel utworzony",
"targetCreatedDescription": "Cel został utworzony pomyślnie",
"targetErrorCreate": "Nie udało się utworzyć celu",
@@ -2346,7 +2348,7 @@
"description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe."
}
},
"personalUseOnly": "Wyłącznie do użytku osobistego (bezpłatna licencja brak zamówień)",
"personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)",
"buttons": {
"continueToCheckout": "Przejdź do zamówienia"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Klienci maszyn",
"install": "Zainstaluj",
"run": "Uruchom",
"envFile": "Plik środowiska",
"serviceFile": "Plik serwisu",
"enableAndStart": "Włącz i Uruchom",
"clientNameDescription": "Wyświetlana nazwa klienta, która może zostać zmieniona później.",
"clientAddress": "Adres klienta (Zaawansowany)",
"setupFailedToFetchSubnet": "Nie udało się pobrać domyślnej podsieci",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Brak uwierzytelniania",
"httpDestAuthNoneDescription": "Wysyła żądania bez nagłówka autoryzacji.",
"httpDestAuthBearerTitle": "Token Bearer",
"httpDestAuthBearerDescription": "Dodaje autoryzację: nagłówek Bearer <token> do każdego żądania.",
"httpDestAuthBearerDescription": "Dodaje nagłówek Authorization: Bearer '<token>' do każdego żądania.",
"httpDestAuthBearerPlaceholder": "Twój klucz API lub token",
"httpDestAuthBasicTitle": "Podstawowa Autoryzacja",
"httpDestAuthBasicDescription": "Dodaje Autoryzacja: Nagłówek Basic <credentials> . Podaj poświadczenia jako nazwę użytkownika: hasło.",
"httpDestAuthBasicDescription": "Dodaje nagłówek Authorization: Basic '<credentials>'. Podaj poświadczenia w formacie użytkownik:hasło.",
"httpDestAuthBasicPlaceholder": "Nazwa użytkownika:hasło",
"httpDestAuthCustomTitle": "Niestandardowy nagłówek",
"httpDestAuthCustomDescription": "Określ niestandardową nazwę nagłówka HTTP i wartość dla uwierzytelniania (np. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Chave de provisionamento atualizada",
"provisioningKeysUpdatedDescription": "Suas alterações foram salvas.",
"provisioningKeysBannerTitle": "Chaves de provisionamento do site",
"provisioningKeysBannerDescription": "Gerar uma chave de provisionamento e usá-la com o conector de Newt para criar automaticamente sites na primeira inicialização — não é necessário configurar credenciais separadas para cada site.",
"provisioningKeysBannerDescription": "Gere uma chave de provisionamento e use-a com o conector Newt para criar sites automaticamente na primeira inicialização - sem necessidade de configurar credenciais separadas para cada site.",
"provisioningKeysBannerButtonText": "Saiba mais",
"pendingSitesBannerTitle": "Sites pendentes",
"pendingSitesBannerDescription": "Sites que conectam usando uma chave de provisionamento aparecem aqui para revisão. Aprovar cada site antes de se tornar ativo e ganhar acesso a seus recursos.",
"pendingSitesBannerDescription": "Sites que se conectam usando uma chave de provisionamento aparecem aqui para revisão.",
"pendingSitesBannerButtonText": "Saiba mais",
"apiKeysSettings": "Configurações de {apiKeyName}",
"userTitle": "Gerir Todos os Utilizadores",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido",
"targetErrorNoSite": "Nenhum site selecionado",
"targetErrorNoSiteDescription": "Selecione um site para o destino",
"targetTargetsCleared": "Alvos limpos",
"targetTargetsClearedDescription": "Todos os alvos foram removidos deste recurso",
"targetCreated": "Destino criado",
"targetCreatedDescription": "O alvo foi criado com sucesso",
"targetErrorCreate": "Falha ao criar destino",
@@ -2346,7 +2348,7 @@
"description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário."
}
},
"personalUseOnly": "Apenas uso pessoal (licença gratuita sem check-out)",
"personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)",
"buttons": {
"continueToCheckout": "Continuar com checkout"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Clientes de máquina",
"install": "Instale",
"run": "Executar",
"envFile": "Arquivo de Ambiente",
"serviceFile": "Arquivo de Serviço",
"enableAndStart": "Ativar e Iniciar",
"clientNameDescription": "O nome de exibição do cliente que pode ser alterado mais tarde.",
"clientAddress": "Endereço do Cliente (Avançado)",
"setupFailedToFetchSubnet": "Falha ao buscar a subrede padrão",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Sem Autenticação",
"httpDestAuthNoneDescription": "Envia pedidos sem um cabeçalho de autorização.",
"httpDestAuthBearerTitle": "Token do portador",
"httpDestAuthBearerDescription": "Adiciona uma autorização: Bearer <token> header a cada requisição.",
"httpDestAuthBearerDescription": "Adiciona um cabeçalho Authorization: Bearer '<token>' a cada solicitação.",
"httpDestAuthBearerPlaceholder": "Sua chave de API ou token",
"httpDestAuthBasicTitle": "Autenticação básica",
"httpDestAuthBasicDescription": "Adiciona uma Autorização: cabeçalho <credentials> básico. Forneça credenciais como nome de usuário:senha.",
"httpDestAuthBasicDescription": "Adiciona um cabeçalho Authorization: Basic '<credentials>'. Forneça as credenciais como username:password.",
"httpDestAuthBasicPlaceholder": "Usuário:password",
"httpDestAuthCustomTitle": "Cabeçalho personalizado",
"httpDestAuthCustomDescription": "Especifique um nome e valor de cabeçalho HTTP personalizado para autenticação (por exemplo, X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Ключ подготовки обновлен",
"provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.",
"provisioningKeysBannerTitle": "Ключи подготовки сайта",
"provisioningKeysBannerDescription": "Генерировать подготовительный ключ и использовать его вместе с Новым коннектором для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
"provisioningKeysBannerButtonText": "Узнать больше",
"pendingSitesBannerTitle": "Ожидающие сайты",
"pendingSitesBannerDescription": "Сайты, связанные с использованием ключа подготовки, появляются здесь для проверки. Одобрите каждый сайт, прежде чем он станет активным и получит доступ к вашим ресурсам.",
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
"pendingSitesBannerButtonText": "Узнать больше",
"apiKeysSettings": "Настройки {apiKeyName}",
"userTitle": "Управление всеми пользователями",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
"targetErrorNoSite": "Сайт не выбран",
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
"targetTargetsCleared": "Цели очищены",
"targetTargetsClearedDescription": "Все цели удалены из этого ресурса",
"targetCreated": "Цель создана",
"targetCreatedDescription": "Цель была успешно создана",
"targetErrorCreate": "Не удалось создать цель",
@@ -2346,7 +2348,7 @@
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка."
}
},
"personalUseOnly": "Только для личного пользования (бесплатная лицензия без оформления)",
"personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)",
"buttons": {
"continueToCheckout": "Продолжить оформление заказа"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Машинные клиенты",
"install": "Установить",
"run": "Запустить",
"envFile": "Файл окружения",
"serviceFile": "Сервисный файл",
"enableAndStart": "Включить и запустить",
"clientNameDescription": "Отображаемое имя клиента, которое может быть изменено позже.",
"clientAddress": "Адрес клиента (Дополнительно)",
"setupFailedToFetchSubnet": "Не удалось получить подсеть по умолчанию",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Нет аутентификации",
"httpDestAuthNoneDescription": "Отправляет запросы без заголовка авторизации.",
"httpDestAuthBearerTitle": "Жетон носителя",
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer <token> к каждому запросу.",
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer '<token>' к каждому запросу.",
"httpDestAuthBearerPlaceholder": "Ваш ключ API или токен",
"httpDestAuthBasicTitle": "Базовая авторизация",
"httpDestAuthBasicDescription": "Добавляет Authorization: Basic <credentials> header. Предоставьте учетные данные в качестве имени пользователя:password.",
"httpDestAuthBasicDescription": "Добавляет заголовок Authorization: Basic '<credentials>'. Укажите учетные данные в формате username:password.",
"httpDestAuthBasicPlaceholder": "имя пользователя:пароль",
"httpDestAuthCustomTitle": "Пользовательский заголовок",
"httpDestAuthCustomDescription": "Укажите пользовательское имя заголовка HTTP и значение для аутентификации (например, X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "Tedarik anahtarı güncellendi",
"provisioningKeysUpdatedDescription": "Değişiklikleriniz kaydedildi.",
"provisioningKeysBannerTitle": "Site Tedarik Anahtarları",
"provisioningKeysBannerDescription": "Tedarik anahtarı oluşturun ve ilk başlangıçta siteleri otomatik olarak oluşturmak için Newt konektörüyle kullanın her site için ayrı kimlik bilgileri ayarlamaya gerek yoktur.",
"provisioningKeysBannerDescription": "Bir sağlama anahtarı oluşturun ve ilk başlangıçta siteleri otomatik olarak oluşturmak için Newt bağlayıcısını kullanın - her site için ayrı kimlik bilgileri ayarlamaya gerek yok.",
"provisioningKeysBannerButtonText": "Daha fazla bilgi",
"pendingSitesBannerTitle": "Bekleyen Siteler",
"pendingSitesBannerDescription": "Tedarik anahtarı kullanarak bağlanan siteler burada incelenmek için görünür. Aktif hale gelmeden ve kaynaklarınıza erişim kazanmadan önce her siteyi onaylayın.",
"pendingSitesBannerDescription": "Bir sağlama anahtarı kullanarak bağlanan siteler, inceleme için burada görünür.",
"pendingSitesBannerButtonText": "Daha fazla bilgi",
"apiKeysSettings": "{apiKeyName} Ayarları",
"userTitle": "Tüm Kullanıcıları Yönet",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin",
"targetErrorNoSite": "Hiçbir site seçili değil",
"targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin",
"targetTargetsCleared": "Hedefler temizlendi",
"targetTargetsClearedDescription": "Bu kaynaktan tüm hedefler kaldırıldı",
"targetCreated": "Hedef oluşturuldu",
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
@@ -2346,7 +2348,7 @@
"description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek."
}
},
"personalUseOnly": "Yalnızca kişisel kullanım (ücretsiz lisans ödeme yapılmaz)",
"personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)",
"buttons": {
"continueToCheckout": "Ödemeye Devam Et"
},
@@ -2607,6 +2609,9 @@
"machineClients": "Makine İstemcileri",
"install": "Yükle",
"run": "Çalıştır",
"envFile": "Ortam Dosyası",
"serviceFile": "Servis Dosyası",
"enableAndStart": "Etkinleştir ve Başlat",
"clientNameDescription": "Daha sonra değiştirilebilecek istemcinin görünen adı.",
"clientAddress": "İstemci Adresi (Gelişmiş)",
"setupFailedToFetchSubnet": "Varsayılan alt ağ alınamadı",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "Kimlik Doğrulama Yok",
"httpDestAuthNoneDescription": "Yetkilendirme başlığı olmadan istekler gönderir.",
"httpDestAuthBearerTitle": "Taşıyıcı Jetonu",
"httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı <token> başlığı ekler.",
"httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı '<token>' üst bilgisi ekler.",
"httpDestAuthBearerPlaceholder": "API anahtarınız veya jetonunuz",
"httpDestAuthBasicTitle": "Temel Kimlik Doğrulama",
"httpDestAuthBasicDescription": "Authorization: Temel <belirtecikler> başlığı ekler. Yetkilendirmeleri kullanıcı adı:şifre olarak sağlayın.",
"httpDestAuthBasicDescription": "Bir Yetkilendirme: Temel '<credentials>' üst bilgisi ekler. Kimlik bilgilerini kullanıcı adı:şifre olarak sağlayın.",
"httpDestAuthBasicPlaceholder": "kullanıcı adı:şifre",
"httpDestAuthCustomTitle": "Özel Başlık",
"httpDestAuthCustomDescription": "Kimlik doğrulama için özel bir HTTP başlık adı ve değer belirtin (örn. X-API-Key).",

View File

@@ -371,10 +371,10 @@
"provisioningKeysUpdated": "置备密钥已更新",
"provisioningKeysUpdatedDescription": "您的更改已保存。",
"provisioningKeysBannerTitle": "站点置备密钥",
"provisioningKeysBannerDescription": "生成一个预配键并使用它来在首次启动时自动创建站点——无需为每个站点设置单独的凭。",
"provisioningKeysBannerDescription": "生成一个供应密钥,并将其与 Newt 连接器一起使用,以在首次启动时自动创建站点 - 无需为每个站点设置单独的凭。",
"provisioningKeysBannerButtonText": "了解更多",
"pendingSitesBannerTitle": "待定站点",
"pendingSitesBannerDescription": "使用预配键连接的站点会出现在这里供审核。在站点开始运行之前批准并获取对您资源的访问权限。",
"pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。",
"pendingSitesBannerButtonText": "了解更多",
"apiKeysSettings": "{apiKeyName} 设置",
"userTitle": "管理所有用户",
@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "请输入有效的端口号",
"targetErrorNoSite": "没有选择站点",
"targetErrorNoSiteDescription": "请选择目标站点",
"targetTargetsCleared": "目标已清除",
"targetTargetsClearedDescription": "所有目标已从此资源中移除",
"targetCreated": "目标已创建",
"targetCreatedDescription": "目标已成功创建",
"targetErrorCreate": "创建目标失败",
@@ -2346,7 +2348,7 @@
"description": "企业特征、50个用户、50个站点和优先支持。"
}
},
"personalUseOnly": "仅个人使用 (免费许可证-无签出)",
"personalUseOnly": "仅个人使用免费许可 - 无需结账)",
"buttons": {
"continueToCheckout": "继续签出"
},
@@ -2607,6 +2609,9 @@
"machineClients": "机器客户端",
"install": "安装",
"run": "运行",
"envFile": "环境文件",
"serviceFile": "服务文件",
"enableAndStart": "启用并启动",
"clientNameDescription": "可以稍后更改的客户端的显示名称。",
"clientAddress": "客户端地址 (高级)",
"setupFailedToFetchSubnet": "获取默认子网失败",
@@ -2845,10 +2850,10 @@
"httpDestAuthNoneTitle": "无身份验证",
"httpDestAuthNoneDescription": "在没有授权头的情况下发送请求。",
"httpDestAuthBearerTitle": "持有者令牌",
"httpDestAuthBearerDescription": "添加授权:每个请求的标题为 <token>。",
"httpDestAuthBearerDescription": "在每个请求中添加授权Bearer “<token>” 头。",
"httpDestAuthBearerPlaceholder": "您的 API 密钥或令牌",
"httpDestAuthBasicTitle": "基本认证",
"httpDestAuthBasicDescription": "添加授权:基本 <credentials> 头。提供用户名:密码凭据。",
"httpDestAuthBasicDescription": "添加一个Authorization: Basic \"<凭据>\" 标头。 以用户名:密码形式提供凭据。",
"httpDestAuthBasicPlaceholder": "用户名:密码",
"httpDestAuthCustomTitle": "自定义标题",
"httpDestAuthCustomDescription": "指定自定义 HTTP 头名称和身份验证值 (例如X-API 键)。",

View File

@@ -479,10 +479,7 @@ export async function getTraefikConfig(
// TODO: HOW TO HANDLE ^^^^^^ BETTER
const anySitesOnline = targets.some(
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
(target) => target.site.online
);
return (
@@ -495,7 +492,7 @@ export async function getTraefikConfig(
if (target.health == "unhealthy") {
return false;
}
// If any sites are online, exclude offline sites
if (anySitesOnline && !target.site.online) {
return false;
@@ -610,10 +607,7 @@ export async function getTraefikConfig(
servers: (() => {
// Check if any sites are online
const anySitesOnline = targets.some(
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
(target) => target.site.online
);
return targets
@@ -621,7 +615,7 @@ export async function getTraefikConfig(
if (!target.enabled) {
return false;
}
// If any sites are online, exclude offline sites
if (anySitesOnline && !target.site.online) {
return false;

View File

@@ -23,6 +23,8 @@ import {
} from "@server/db";
import logger from "@server/logger";
import { and, eq, gt, desc, max, sql } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import {
LogType,
LOG_TYPES,
@@ -127,7 +129,7 @@ export class LogStreamingManager {
start(): void {
if (this.isRunning) return;
this.isRunning = true;
logger.info("LogStreamingManager: started");
logger.debug("LogStreamingManager: started");
this.schedulePoll(POLL_INTERVAL_MS);
}
@@ -272,19 +274,20 @@ export class LogStreamingManager {
return;
}
// Parse config skip destination if config is unparseable
let config: HttpConfig;
// Decrypt and parse config skip destination if either step fails
let configFromDb: HttpConfig;
try {
config = JSON.parse(dest.config) as HttpConfig;
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
} catch (err) {
logger.error(
`LogStreamingManager: destination ${dest.destinationId} has invalid JSON config`,
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
err
);
return;
}
const provider = this.createProvider(dest.type, config);
const provider = this.createProvider(dest.type, configFromDb);
if (!provider) {
logger.warn(
`LogStreamingManager: unsupported destination type "${dest.type}" ` +
@@ -770,4 +773,4 @@ export class LogStreamingManager {
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -671,10 +671,7 @@ export async function getTraefikConfig(
// TODO: HOW TO HANDLE ^^^^^^ BETTER
const anySitesOnline = targets.some(
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
(target) => target.site.online
);
return (
@@ -802,10 +799,7 @@ export async function getTraefikConfig(
servers: (() => {
// Check if any sites are online
const anySitesOnline = targets.some(
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
(target) => target.site.online
);
return targets

View File

@@ -22,6 +22,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -87,7 +89,10 @@ export async function createEventStreamingDestination(
);
}
const { type, config, enabled } = parsedBody.data;
const { type, config: configToSet, enabled } = parsedBody.data;
const key = config.getRawConfig().server.secret!;
const encryptedConfig = encrypt(configToSet, key);
const now = Date.now();
@@ -96,7 +101,7 @@ export async function createEventStreamingDestination(
.values({
orgId,
type,
config,
config: encryptedConfig,
enabled,
createdAt: now,
updatedAt: now,

View File

@@ -22,6 +22,8 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { eq, sql } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -121,9 +123,22 @@ export async function listEventStreamingDestinations(
.from(eventStreamingDestinations)
.where(eq(eventStreamingDestinations.orgId, orgId));
const key = config.getRawConfig().server.secret!;
const decryptedList = list.map((dest) => {
try {
return { ...dest, config: decrypt(dest.config, key) };
} catch (err) {
logger.error(
`listEventStreamingDestinations: failed to decrypt config for destination ${dest.destinationId}`,
err
);
return { ...dest, config: "" };
}
});
return response<ListEventStreamingDestinationsResponse>(res, {
data: {
destinations: list,
destinations: decryptedList,
pagination: {
total: count,
limit,

View File

@@ -22,7 +22,8 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
const paramsSchema = z
.object({
@@ -110,14 +111,17 @@ export async function updateEventStreamingDestination(
);
}
const { type, config, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
const updateData: Record<string, unknown> = {
updatedAt: Date.now()
};
if (type !== undefined) updateData.type = type;
if (config !== undefined) updateData.config = config;
if (configToUpdate !== undefined) {
const key = config.getRawConfig().server.secret!;
updateData.config = encrypt(configToUpdate, key);
}
if (enabled !== undefined) updateData.enabled = enabled;
if (sendAccessLogs !== undefined) updateData.sendAccessLogs = sendAccessLogs;
if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;

View File

@@ -171,9 +171,8 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
}
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
const valuesList = chunk.map(
([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}, ${bytesIn}, ${bytesOut})`
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
);
const valuesClause = sql.join(valuesList, sql`, `);
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`

View File

@@ -8,6 +8,7 @@ import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
import { convertTargetsIfNessicary } from "../client/targets";
import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
export const handleGetConfigMessage: MessageHandler = async (context) => {
const { message, client, sendToClient } = context;
@@ -55,7 +56,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
logger.warn(
`handleGetConfigMessage: Site ${existingSite.siteId} last hole punch is too old, skipping`
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
);
return;
}

View File

@@ -1,4 +1,4 @@
import { db, newts, sites } from "@server/db";
import { db, newts, sites, targetHealthCheck, targets } from "@server/db";
import {
hasActiveConnections,
getClientConfigVersion
@@ -78,6 +78,32 @@ export const startNewtOfflineChecker = (): void => {
.update(sites)
.set({ online: false })
.where(eq(sites.siteId, staleSite.siteId));
const healthChecksOnSite = await db
.select()
.from(targetHealthCheck)
.innerJoin(
targets,
eq(targets.targetId, targetHealthCheck.targetId)
)
.innerJoin(sites, eq(sites.siteId, targets.siteId))
.where(eq(sites.siteId, staleSite.siteId));
for (const healthCheck of healthChecksOnSite) {
logger.info(
`Marking health check ${healthCheck.targetHealthCheck.targetHealthCheckId} offline due to site ${staleSite.siteId} being marked offline`
);
await db
.update(targetHealthCheck)
.set({ hcHealth: "unknown" })
.where(
eq(
targetHealthCheck.targetHealthCheckId,
healthCheck.targetHealthCheck
.targetHealthCheckId
)
);
}
}
// this part only effects self hosted. Its not efficient but we dont expect people to have very many wireguard sites
@@ -102,7 +128,8 @@ export const startNewtOfflineChecker = (): void => {
// loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline
for (const site of allWireguardSites) {
const lastBandwidthUpdate = new Date(site.lastBandwidthUpdate!).getTime() / 1000;
const lastBandwidthUpdate =
new Date(site.lastBandwidthUpdate!).getTime() / 1000;
if (
lastBandwidthUpdate < wireguardOfflineThreshold &&
site.online

View File

@@ -1,6 +1,6 @@
import { db } from "@server/db";
import { sites, clients, olms } from "@server/db";
import { eq, inArray } from "drizzle-orm";
import { inArray } from "drizzle-orm";
import logger from "@server/logger";
/**
@@ -21,7 +21,7 @@ import logger from "@server/logger";
*/
const FLUSH_INTERVAL_MS = 10_000; // Flush every 10 seconds
const MAX_RETRIES = 2;
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 50;
// ── Site (newt) pings ──────────────────────────────────────────────────
@@ -36,6 +36,14 @@ const pendingOlmArchiveResets: Set<string> = new Set();
let flushTimer: NodeJS.Timeout | null = null;
/**
* Guard that prevents two flush cycles from running concurrently.
* setInterval does not await async callbacks, so without this a slow flush
* (e.g. due to DB latency) would overlap with the next scheduled cycle and
* the two concurrent bulk UPDATEs would deadlock each other.
*/
let isFlushing = false;
// ── Public API ─────────────────────────────────────────────────────────
/**
@@ -72,6 +80,12 @@ export function recordClientPing(
/**
* Flush all accumulated site pings to the database.
*
* Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE
* statement. We use the maximum timestamp across the batch so that `lastPing`
* reflects the most recent ping seen for any site in the group. This avoids
* the multi-statement transaction that previously created additional
* row-lock ordering hazards.
*/
async function flushSitePingsToDb(): Promise<void> {
if (pendingSitePings.size === 0) {
@@ -83,55 +97,35 @@ async function flushSitePingsToDb(): Promise<void> {
const pingsToFlush = new Map(pendingSitePings);
pendingSitePings.clear();
// Sort by siteId for consistent lock ordering (prevents deadlocks)
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
([a], [b]) => a - b
);
const entries = Array.from(pingsToFlush.entries());
const BATCH_SIZE = 50;
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
const batch = entries.slice(i, i + BATCH_SIZE);
// Use the latest timestamp in the batch so that `lastPing` always
// moves forward. Using a single timestamp for the whole batch means
// we only ever need one UPDATE statement (no transaction).
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
const siteIds = batch.map(([id]) => id);
try {
await withRetry(async () => {
// Group by timestamp for efficient bulk updates
const byTimestamp = new Map<number, number[]>();
for (const [siteId, timestamp] of batch) {
const group = byTimestamp.get(timestamp) || [];
group.push(siteId);
byTimestamp.set(timestamp, group);
}
if (byTimestamp.size === 1) {
const [timestamp, siteIds] = Array.from(
byTimestamp.entries()
)[0];
await db
.update(sites)
.set({
online: true,
lastPing: timestamp
})
.where(inArray(sites.siteId, siteIds));
} else {
await db.transaction(async (tx) => {
for (const [timestamp, siteIds] of byTimestamp) {
await tx
.update(sites)
.set({
online: true,
lastPing: timestamp
})
.where(inArray(sites.siteId, siteIds));
}
});
}
await db
.update(sites)
.set({
online: true,
lastPing: maxTimestamp
})
.where(inArray(sites.siteId, siteIds));
}, "flushSitePingsToDb");
} catch (error) {
logger.error(
`Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`,
{ error }
);
// Re-queue only if the preserved timestamp is newer than any
// update that may have landed since we snapshotted.
for (const [siteId, timestamp] of batch) {
const existing = pendingSitePings.get(siteId);
if (!existing || existing < timestamp) {
@@ -144,6 +138,8 @@ async function flushSitePingsToDb(): Promise<void> {
/**
* Flush all accumulated client (OLM) pings to the database.
*
* Same single-UPDATE-per-batch approach as `flushSitePingsToDb`.
*/
async function flushClientPingsToDb(): Promise<void> {
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
@@ -159,51 +155,25 @@ async function flushClientPingsToDb(): Promise<void> {
// ── Flush client pings ─────────────────────────────────────────────
if (pingsToFlush.size > 0) {
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
([a], [b]) => a - b
);
const entries = Array.from(pingsToFlush.entries());
const BATCH_SIZE = 50;
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
const batch = entries.slice(i, i + BATCH_SIZE);
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
const clientIds = batch.map(([id]) => id);
try {
await withRetry(async () => {
const byTimestamp = new Map<number, number[]>();
for (const [clientId, timestamp] of batch) {
const group = byTimestamp.get(timestamp) || [];
group.push(clientId);
byTimestamp.set(timestamp, group);
}
if (byTimestamp.size === 1) {
const [timestamp, clientIds] = Array.from(
byTimestamp.entries()
)[0];
await db
.update(clients)
.set({
lastPing: timestamp,
online: true,
archived: false
})
.where(inArray(clients.clientId, clientIds));
} else {
await db.transaction(async (tx) => {
for (const [timestamp, clientIds] of byTimestamp) {
await tx
.update(clients)
.set({
lastPing: timestamp,
online: true,
archived: false
})
.where(
inArray(clients.clientId, clientIds)
);
}
});
}
await db
.update(clients)
.set({
lastPing: maxTimestamp,
online: true,
archived: false
})
.where(inArray(clients.clientId, clientIds));
}, "flushClientPingsToDb");
} catch (error) {
logger.error(
@@ -260,7 +230,12 @@ export async function flushPingsToDb(): Promise<void> {
/**
* Simple retry wrapper with exponential backoff for transient errors
* (connection timeouts, unexpected disconnects).
* (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>,
@@ -277,7 +252,8 @@ async function withRetry<T>(
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`
`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;
@@ -288,14 +264,14 @@ async function withRetry<T>(
}
/**
* Detect transient connection errors that are safe to retry.
* 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 || "";
const code = error.code || error.cause?.code || "";
// Connection timeout / terminated
if (
@@ -308,12 +284,17 @@ function isTransientError(error: any): boolean {
return true;
}
// PostgreSQL deadlock
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
if (code === "40P01" || message.includes("deadlock")) {
return true;
}
// ECONNRESET, ECONNREFUSED, EPIPE
// PostgreSQL serialization failure
if (code === "40001") {
return true;
}
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
@@ -337,12 +318,26 @@ export function startPingAccumulator(): void {
}
flushTimer = setInterval(async () => {
// Skip this tick if the previous flush is still in progress.
// setInterval does not await async callbacks, so without this guard
// two flush cycles can run concurrently and deadlock each other on
// overlapping bulk UPDATE statements.
if (isFlushing) {
logger.debug(
"Ping accumulator: previous flush still in progress, skipping cycle"
);
return;
}
isFlushing = true;
try {
await flushPingsToDb();
} catch (error) {
logger.error("Unhandled error in ping accumulator flush", {
error
});
} finally {
isFlushing = false;
}
}, FLUSH_INTERVAL_MS);
@@ -364,7 +359,22 @@ export async function stopPingAccumulator(): Promise<void> {
flushTimer = null;
}
// Final flush to persist any remaining pings
// Final flush to persist any remaining pings.
// Wait for any in-progress flush to finish first so we don't race.
if (isFlushing) {
logger.debug(
"Ping accumulator: waiting for in-progress flush before stopping…"
);
await new Promise<void>((resolve) => {
const poll = setInterval(() => {
if (!isFlushing) {
clearInterval(poll);
resolve();
}
}, 50);
});
}
try {
await flushPingsToDb();
} catch (error) {
@@ -379,4 +389,4 @@ export async function stopPingAccumulator(): Promise<void> {
*/
export function getPendingPingCount(): number {
return pendingSitePings.size + pendingClientPings.size;
}
}

View File

@@ -27,7 +27,7 @@ import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { INSPECT_MAX_BYTES } from "buffer";
import { v } from "@faker-js/faker/dist/airline-Dz1uGqgJ";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
const bodySchema = z.object({
provisioningKey: z.string().nonempty(),
@@ -152,6 +152,11 @@ export async function registerNewt(
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
);
}
if (!org.subnet) {
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Organization subnet not found")
);
}
// SaaS billing check
if (build == "saas") {
@@ -190,6 +195,20 @@ export async function registerNewt(
let newSiteId: number | undefined;
await db.transaction(async (trx) => {
const newClientAddress = await getNextAvailableClientSubnet(orgId);
if (!newClientAddress) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"No available subnet found"
)
);
}
let clientAddress = newClientAddress.split("/")[0];
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
// Create the site (type "newt", name = niceId)
const [newSite] = await trx
.insert(sites)
@@ -197,6 +216,7 @@ export async function registerNewt(
orgId,
name: name || niceId,
niceId,
address: clientAddress,
type: "newt",
dockerSocketEnabled: true,
status: keyRecord.approveNewSites ? "approved" : "pending",

View File

@@ -20,6 +20,7 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils";
import { Alias } from "@server/lib/ip";
import { build } from "@server/build";
import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
logger.info("Handling register olm message!");
@@ -274,7 +275,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
// TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
logger.warn(
"Client last hole punch is too old and we have sites to send; skipping this register"
`Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
);
return;
}

View File

@@ -77,7 +77,8 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
const [targetCheck] = await db
.select({
targetId: targets.targetId,
siteId: targets.siteId
siteId: targets.siteId,
hcStatus: targetHealthCheck.hcHealth
})
.from(targets)
.innerJoin(
@@ -85,6 +86,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
eq(targets.resourceId, resources.resourceId)
)
.innerJoin(sites, eq(targets.siteId, sites.siteId))
.innerJoin(targetHealthCheck, eq(targets.targetId, targetHealthCheck.targetId))
.where(
and(
eq(targets.targetId, targetIdNum),
@@ -101,6 +103,14 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
continue;
}
// check if the status has changed
if (targetCheck.hcStatus === healthStatus.status) {
logger.debug(
`Health status for target ${targetId} is already ${healthStatus.status}, skipping update`
);
continue;
}
// Update the target's health status in the database
await db
.update(targetHealthCheck)

View File

@@ -104,6 +104,42 @@ export default async function migration() {
CONSTRAINT "userOrgRoles_userId_orgId_roleId_unique" UNIQUE("userId","orgId","roleId")
);
`);
await db.execute(sql`
CREATE TABLE "eventStreamingCursors" (
"cursorId" serial PRIMARY KEY NOT NULL,
"destinationId" integer NOT NULL,
"logType" varchar(50) NOT NULL,
"lastSentId" bigint DEFAULT 0 NOT NULL,
"lastSentAt" bigint
);
`);
await db.execute(sql`
CREATE TABLE "eventStreamingDestinations" (
"destinationId" serial PRIMARY KEY NOT NULL,
"orgId" varchar(255) NOT NULL,
"sendConnectionLogs" boolean DEFAULT false NOT NULL,
"sendRequestLogs" boolean DEFAULT false NOT NULL,
"sendActionLogs" boolean DEFAULT false NOT NULL,
"sendAccessLogs" boolean DEFAULT false NOT NULL,
"type" varchar(50) NOT NULL,
"config" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"createdAt" bigint NOT NULL,
"updatedAt" bigint NOT NULL
);
`);
await db.execute(
sql`ALTER TABLE "eventStreamingCursors" ADD CONSTRAINT "eventStreamingCursors_destinationId_eventStreamingDestinations_destinationId_fk" FOREIGN KEY ("destinationId") REFERENCES "public"."eventStreamingDestinations"("destinationId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "eventStreamingDestinations" ADD CONSTRAINT "eventStreamingDestinations_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`CREATE UNIQUE INDEX "idx_eventStreamingCursors_dest_type" ON "eventStreamingCursors" USING btree ("destinationId","logType");`
);
await db.execute(
sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";`
);
@@ -177,8 +213,12 @@ export default async function migration() {
sql`CREATE INDEX "idx_accessAuditLog_siteResourceId" ON "connectionAuditLog" USING btree ("siteResourceId");`
);
await db.execute(sql`ALTER TABLE "userInvites" DROP COLUMN "roleId";`);
await db.execute(sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`);
await db.execute(sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`);
await db.execute(
sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`
);
await db.execute(sql`COMMIT`);
console.log("Migrated database");

View File

@@ -76,9 +76,15 @@ export default async function migration() {
`
).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`
).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`
).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`
).run();
db.prepare(
`
@@ -168,6 +174,42 @@ export default async function migration() {
);
`
).run();
db.prepare(
`
CREATE TABLE 'eventStreamingCursors' (
'cursorId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'destinationId' integer NOT NULL,
'logType' text NOT NULL,
'lastSentId' integer DEFAULT 0 NOT NULL,
'lastSentAt' integer,
FOREIGN KEY ('destinationId') REFERENCES 'eventStreamingDestinations'('destinationId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'idx_eventStreamingCursors_dest_type' ON 'eventStreamingCursors' ('destinationId','logType');--> statement-breakpoint
`
).run();
db.prepare(
`
CREATE TABLE 'eventStreamingDestinations' (
'destinationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'orgId' text NOT NULL,
'sendConnectionLogs' integer DEFAULT false NOT NULL,
'sendRequestLogs' integer DEFAULT false NOT NULL,
'sendActionLogs' integer DEFAULT false NOT NULL,
'sendAccessLogs' integer DEFAULT false NOT NULL,
'type' text NOT NULL,
'config' text NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'createdAt' integer NOT NULL,
'updatedAt' integer NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`INSERT INTO '__new_userInvites'("inviteId", "orgId", "email", "expiresAt", "token") SELECT "inviteId", "orgId", "email", "expiresAt", "token" FROM 'userInvites';`
).run();
@@ -191,8 +233,12 @@ export default async function migration() {
`ALTER TABLE 'user' ADD 'marketingEmailConsent' integer DEFAULT false;`
).run();
db.prepare(`ALTER TABLE 'user' ADD 'locale' text;`).run();
db.prepare(`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`).run();
db.prepare(`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`).run();
db.prepare(
`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`
).run();
db.prepare(
`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`
).run();
})();
db.pragma("foreign_keys = ON");

View File

@@ -491,7 +491,7 @@ export default function ConnectionLogsPage() {
);
},
cell: ({ row }) => {
const clientType = row.original.clientType === "olm" ? "machine" : "user";
const clientType = row.original.userId ? "user" : "machine";
if (row.original.clientName && row.original.clientNiceId) {
return (
<Link

View File

@@ -106,7 +106,9 @@ function DestinationCard({
{/* URL preview */}
<p className="text-xs text-muted-foreground truncate">
{cfg.url || (
<span className="italic">{t("streamingNoUrlConfigured")}</span>
<span className="italic">
{t("streamingNoUrlConfigured")}
</span>
)}
</p>
@@ -160,7 +162,9 @@ function AddDestinationCard({ onClick }: { onClick: () => void }) {
<div className="flex items-center justify-center w-9 h-9 rounded-md border-2 border-dashed border-current">
<Plus className="h-4 w-4" />
</div>
<span className="text-sm font-medium">{t("streamingAddDestination")}</span>
<span className="text-sm font-medium">
{t("streamingAddDestination")}
</span>
</div>
</button>
);
@@ -186,7 +190,9 @@ function DestinationTypePicker({
const t = useTranslations();
const [selected, setSelected] = useState<DestinationType>("http");
const destinationTypeOptions: ReadonlyArray<StrategyOption<DestinationType>> = [
const destinationTypeOptions: ReadonlyArray<
StrategyOption<DestinationType>
> = [
{
id: "http",
title: t("streamingHttpWebhookTitle"),
@@ -233,13 +239,19 @@ function DestinationTypePicker({
<Credenza open={open} onOpenChange={onOpenChange}>
<CredenzaContent className="sm:max-w-lg">
<CredenzaHeader>
<CredenzaTitle>{t("streamingAddDestination")}</CredenzaTitle>
<CredenzaTitle>
{t("streamingAddDestination")}
</CredenzaTitle>
<CredenzaDescription>
{t("streamingTypePickerDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className={isPaywalled ? "pointer-events-none opacity-50" : ""}>
<div
className={
isPaywalled ? "pointer-events-none opacity-50" : ""
}
>
<StrategySelect
options={destinationTypeOptions}
value={selected}
@@ -301,10 +313,7 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToLoad"),
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
description: formatAxiosError(e, t("streamingUnexpectedError"))
});
} finally {
setLoading(false);
@@ -341,10 +350,7 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToUpdate"),
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
description: formatAxiosError(e, t("streamingUnexpectedError"))
});
} finally {
setTogglingIds((prev) => {
@@ -375,10 +381,7 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToDelete"),
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
description: formatAxiosError(e, t("streamingUnexpectedError"))
});
} finally {
setDeleting(false);
@@ -459,13 +462,14 @@ export default function StreamingDestinationsPage() {
if (!v) setDeleteTarget(null);
}}
string={
parseHttpConfig(deleteTarget.config).name || t("streamingDeleteDialogThisDestination")
parseHttpConfig(deleteTarget.config).name ||
t("streamingDeleteDialogThisDestination")
}
title={t("streamingDeleteTitle")}
dialog={
<p className="text-sm text-muted-foreground">
<p>
{t("streamingDeleteDialogAreYouSure")}{" "}
<span className="font-semibold text-foreground">
<span>
{parseHttpConfig(deleteTarget.config).name ||
t("streamingDeleteDialogThisDestination")}
</span>
@@ -478,4 +482,4 @@ export default function StreamingDestinationsPage() {
)}
</>
);
}
}

View File

@@ -9,6 +9,8 @@ import DismissableBanner from "@app/components/DismissableBanner";
import Link from "next/link";
import { Button } from "@app/components/ui/button";
import { ArrowRight, Plug } from "lucide-react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
type PendingSitesPageProps = {
params: Promise<{ orgId: string }>;
@@ -96,6 +98,10 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) {
</Button>
</Link>
</DismissableBanner>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.SiteProvisioningKeys]}
/>
<PendingSitesTable
sites={siteRows}
orgId={params.orgId}

View File

@@ -400,7 +400,11 @@ function ProxyResourceTargetsForm({
pathMatchType: row.original.pathMatchType
}}
onChange={(config) =>
updateTarget(row.original.targetId, config)
updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
}
trigger={
<Button
@@ -424,7 +428,11 @@ function ProxyResourceTargetsForm({
pathMatchType: row.original.pathMatchType
}}
onChange={(config) =>
updateTarget(row.original.targetId, config)
updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
}
trigger={
<Button

View File

@@ -776,7 +776,11 @@ export default function Page() {
pathMatchType: row.original.pathMatchType
}}
onChange={(config) =>
updateTarget(row.original.targetId, config)
updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
}
trigger={
<Button
@@ -800,7 +804,11 @@ export default function Page() {
pathMatchType: row.original.pathMatchType
}}
onChange={(config) =>
updateTarget(row.original.targetId, config)
updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
}
trigger={
<Button

View File

@@ -614,6 +614,7 @@ export function InternalResourceForm({
<SitesSelector
orgId={orgId}
selectedSite={selectedSite}
filterTypes={["newt"]}
onSelectSite={(site) => {
setSelectedSite(site);
field.onChange(site.siteId);

View File

@@ -15,6 +15,8 @@ import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { build } from "@server/build";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { type PaginationState } from "@tanstack/react-table";
import {
ArrowDown01Icon,
@@ -63,6 +65,10 @@ export default function PendingSitesTable({
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const canUseSiteProvisioning =
isPaidUser(tierMatrix[TierFeature.SiteProvisioningKeys]) &&
build !== "oss";
const booleanSearchFilterSchema = z
.enum(["true", "false"])
@@ -450,6 +456,7 @@ export default function PendingSitesTable({
onSearch={handleSearchChange}
onRefresh={refreshData}
isRefreshing={isRefreshing || isFiltering}
refreshButtonDisabled={!canUseSiteProvisioning}
rowCount={rowCount}
columnVisibility={{
niceId: false,

View File

@@ -311,6 +311,7 @@ export default function SiteProvisioningKeysTable({
addButtonDisabled={!canUseSiteProvisioning}
onRefresh={refreshData}
isRefreshing={isRefreshing}
refreshButtonDisabled={!canUseSiteProvisioning}
addButtonText={t("provisioningKeysAdd")}
enableColumnVisibility={true}
stickyLeftColumn="name"

View File

@@ -10,14 +10,14 @@ import {
import { CheckboxWithLabel } from "./ui/checkbox";
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
import { useState } from "react";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
import { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
import { SiKubernetes, SiNixos } from "react-icons/si";
export type CommandItem = string | { title: string; command: string };
const PLATFORMS = [
"unix",
"linux",
"macos",
"docker",
"kubernetes",
"podman",
@@ -43,7 +43,7 @@ export function NewtSiteInstallCommands({
const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix");
const [platform, setPlatform] = useState<Platform>("linux");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
@@ -54,8 +54,68 @@ export function NewtSiteInstallCommands({
: "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
unix: {
All: [
linux: {
Run: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("run"),
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
],
"Systemd Service": [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("envFile"),
command: `# Create the directory and environment file
sudo install -d -m 0755 /etc/newt
sudo tee /etc/newt/newt.env > /dev/null << 'EOF'
NEWT_ID=${id}
NEWT_SECRET=${secret}
PANGOLIN_ENDPOINT=${endpoint}${!acceptClients ? `
DISABLE_CLIENTS=true` : ""}
EOF
sudo chmod 600 /etc/newt/newt.env`
},
{
title: t("serviceFile"),
command: `sudo tee /etc/systemd/system/newt.service > /dev/null << 'EOF'
[Unit]
Description=Newt
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=root
Group=root
EnvironmentFile=/etc/newt/newt.env
ExecStart=/usr/local/bin/newt
Restart=always
RestartSec=2
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF`
},
{
title: t("enableAndStart"),
command: `sudo systemctl daemon-reload
sudo systemctl enable --now newt`
}
]
},
macos: {
Run: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
@@ -131,7 +191,7 @@ WantedBy=default.target`
]
},
nixos: {
All: [
Flake: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
}
@@ -172,9 +232,9 @@ WantedBy=default.target`
<OptionSelect<string>
label={
["docker", "podman"].includes(platform)
? t("method")
: t("architecture")
platform === "windows"
? t("architecture")
: t("method")
}
options={getArchitectures(platform).map((arch) => ({
value: arch,
@@ -261,8 +321,10 @@ function getPlatformIcon(platformName: Platform) {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "linux":
return <FaLinux className="h-4 w-4 mr-2" />;
case "macos":
return <FaApple className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
@@ -272,7 +334,7 @@ function getPlatformIcon(platformName: Platform) {
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
default:
return <Terminal className="h-4 w-4 mr-2" />;
return <FaLinux className="h-4 w-4 mr-2" />;
}
}
@@ -280,8 +342,10 @@ function getPlatformName(platformName: Platform) {
switch (platformName) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "linux":
return "Linux";
case "macos":
return "macOS";
case "docker":
return "Docker";
case "kubernetes":
@@ -291,14 +355,16 @@ function getPlatformName(platformName: Platform) {
case "nixos":
return "NixOS";
default:
return "Unix / macOS";
return "Linux";
}
}
function getArchitectures(platform: Platform) {
switch (platform) {
case "unix":
return ["All"];
case "linux":
return ["Run", "Systemd Service"];
case "macos":
return ["Run"];
case "windows":
return ["x64"];
case "docker":
@@ -308,8 +374,8 @@ function getArchitectures(platform: Platform) {
case "podman":
return ["Podman Quadlet", "Podman Run"];
case "nixos":
return ["All"];
return ["Flake"];
default:
return ["x64"];
return ["Run"];
}
}

View File

@@ -24,12 +24,14 @@ export type SitesSelectorProps = {
orgId: string;
selectedSite?: Selectedsite | null;
onSelectSite: (selected: Selectedsite) => void;
filterTypes?: string[];
};
export function SitesSelector({
orgId,
selectedSite,
onSelectSite
onSelectSite,
filterTypes
}: SitesSelectorProps) {
const t = useTranslations();
const [siteSearchQuery, setSiteSearchQuery] = useState("");
@@ -45,7 +47,9 @@ export function SitesSelector({
// always include the selected site in the list of sites shown
const sitesShown = useMemo(() => {
const allSites: Array<Selectedsite> = [...sites];
const allSites: Array<Selectedsite> = filterTypes
? sites.filter((s) => filterTypes.includes(s.type))
: [...sites];
if (
debouncedQuery.trim().length === 0 &&
selectedSite &&
@@ -54,7 +58,7 @@ export function SitesSelector({
allSites.unshift(selectedSite);
}
return allSites;
}, [debouncedQuery, sites, selectedSite]);
}, [debouncedQuery, sites, selectedSite, filterTypes]);
return (
<Command shouldFilter={false}>

View File

@@ -69,6 +69,7 @@ type ControlledDataTableProps<TData, TValue> = {
onAdd?: () => void;
onRefresh?: () => void;
isRefreshing?: boolean;
refreshButtonDisabled?: boolean;
isNavigatingToAddPage?: boolean;
searchPlaceholder?: string;
filters?: DataTableFilter[];
@@ -91,6 +92,7 @@ export function ControlledDataTable<TData, TValue>({
onAdd,
onRefresh,
isRefreshing,
refreshButtonDisabled = false,
searchPlaceholder = "Search...",
filters,
filterDisplayMode = "label",
@@ -335,7 +337,7 @@ export function ControlledDataTable<TData, TValue>({
<Button
variant="outline"
onClick={onRefresh}
disabled={isRefreshing}
disabled={isRefreshing || refreshButtonDisabled}
>
<RefreshCw
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}

View File

@@ -174,6 +174,7 @@ type DataTableProps<TData, TValue> = {
addButtonDisabled?: boolean;
onRefresh?: () => void;
isRefreshing?: boolean;
refreshButtonDisabled?: boolean;
searchPlaceholder?: string;
searchColumn?: string;
defaultSort?: {
@@ -207,6 +208,7 @@ export function DataTable<TData, TValue>({
addButtonDisabled = false,
onRefresh,
isRefreshing,
refreshButtonDisabled = false,
searchPlaceholder = "Search...",
searchColumn = "name",
defaultSort,
@@ -624,7 +626,7 @@ export function DataTable<TData, TValue>({
<Button
variant="outline"
onClick={onRefresh}
disabled={isRefreshing}
disabled={isRefreshing || refreshButtonDisabled}
>
<RefreshCw
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}

View File

@@ -22,12 +22,21 @@ export async function getUserLocale(): Promise<Locale> {
const res = await internal.get("/user", await authCookieHeader());
const userLocale = res.data?.data?.locale;
if (userLocale && locales.includes(userLocale as Locale)) {
// Set the cookie so subsequent requests don't need the API call
(await cookies()).set(COOKIE_NAME, userLocale, {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax"
});
// Try to cache in a cookie so subsequent requests skip the API
// call. cookies().set() is only permitted in Server Actions and
// Route Handlers — not during rendering — so we isolate it so
// that a write failure doesn't prevent the locale from being
// returned for the current request.
try {
(await cookies()).set(COOKIE_NAME, userLocale, {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax"
});
} catch {
// Cannot set cookies in this context (e.g. during rendering);
// the correct locale is still returned below.
}
return userLocale as Locale;
}
} catch {