Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
875138a23d Bump the prod-patch-updates group across 1 directory with 9 updates
Bumps the prod-patch-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.8` | `1.0.11` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `2.0.4` | `2.0.5` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.5` | `2.0.7` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.45.1` | `0.45.2` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.3.0` | `8.3.2` |
| [ioredis](https://github.com/luin/ioredis) | `5.10.0` | `5.10.1` |
| [maxmind](https://github.com/runk/node-maxmind) | `5.0.5` | `5.0.6` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.28.0` | `5.28.10` |
| [use-debounce](https://github.com/xnimorz/use-debounce) | `10.1.0` | `10.1.1` |



Updates `@react-email/components` from 1.0.8 to 1.0.11
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.11/packages/components)

Updates `@react-email/render` from 2.0.4 to 2.0.5
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@2.0.5/packages/render)

Updates `@react-email/tailwind` from 2.0.5 to 2.0.7
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.7/packages/tailwind)

Updates `drizzle-orm` from 0.45.1 to 0.45.2
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.45.1...0.45.2)

Updates `express-rate-limit` from 8.3.0 to 8.3.2
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.3.0...v8.3.2)

Updates `ioredis` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.10.0...v5.10.1)

Updates `maxmind` from 5.0.5 to 5.0.6
- [Release notes](https://github.com/runk/node-maxmind/releases)
- [Commits](https://github.com/runk/node-maxmind/compare/v5.0.5...v5.0.6)

Updates `posthog-node` from 5.28.0 to 5.28.10
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.28.10/packages/node)

Updates `use-debounce` from 10.1.0 to 10.1.1
- [Release notes](https://github.com/xnimorz/use-debounce/releases)
- [Changelog](https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xnimorz/use-debounce/commits)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 1.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/render"
  dependency-version: 2.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: drizzle-orm
  dependency-version: 0.45.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: express-rate-limit
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: ioredis
  dependency-version: 5.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: maxmind
  dependency-version: 5.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.28.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: use-debounce
  dependency-version: 10.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 05:03:56 +00:00
47 changed files with 357 additions and 716 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,8 +86,6 @@ 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,8 +624,6 @@
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
"targetErrorNoSite": "Няма избран сайт",
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
"targetTargetsCleared": "Мишените са премахнати",
"targetTargetsClearedDescription": "Всички цели са били премахнати от този ресурс",
"targetCreated": "Целта е създадена",
"targetCreatedDescription": "Целта беше успешно създадена",
"targetErrorCreate": "Неуспешно създаване на целта",
@@ -2348,7 +2346,7 @@
"description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка."
}
},
"personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)",
"personalUseOnly": "Само за лична употреба (безплатен лиценз без плащане)",
"buttons": {
"continueToCheckout": "Продължете към плащане"
},
@@ -2609,9 +2607,6 @@
"machineClients": "Машинни клиенти",
"install": "Инсталирай",
"run": "Изпълни",
"envFile": "Файл за среда",
"serviceFile": "Файл за услуга",
"enableAndStart": "Активиране и стартиране",
"clientNameDescription": "Показваното име на клиента, което може да се промени по-късно.",
"clientAddress": "Клиентски адрес (Разширено)",
"setupFailedToFetchSubnet": "Неуспешно извличане на подмрежа по подразбиране",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Без удостоверяване",
"httpDestAuthNoneDescription": "Изпращане на заявки без заглавие за удостоверяване.",
"httpDestAuthBearerTitle": "Bearer Токен",
"httpDestAuthBearerDescription": "Добавя заглавие Authorization: Bearer '<token>' към всяка заявка.",
"httpDestAuthBearerDescription": "Добавя заглавие за удостоверяване Bearer <token> към всяка заявка.",
"httpDestAuthBearerPlaceholder": "Вашият API ключ или токен",
"httpDestAuthBasicTitle": "Основно удостоверяване",
"httpDestAuthBasicDescription": "Добавя заглавие Authorization: Basic '<credentials>'. Осигурете идентификационни данни като потребителско име:парола.",
"httpDestAuthBasicDescription": "Добавя заглавие за удостоверяване 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 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.",
"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.",
"provisioningKeysBannerButtonText": "Zjistit více",
"pendingSitesBannerTitle": "Nevyřízené weby",
"pendingSitesBannerDescription": "Stránky, které se připojují pomocí klíče pro zřízení, se zde objeví ke kontrole.",
"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.",
"pendingSitesBannerButtonText": "Zjistit více",
"apiKeysSettings": "Nastavení {apiKeyName}",
"userTitle": "Spravovat všechny uživatele",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory."
}
},
"personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)",
"personalUseOnly": "Pouze osobní použití (bezplatná licence bez platby)",
"buttons": {
"continueToCheckout": "Pokračovat do pokladny"
},
@@ -2609,9 +2607,6 @@
"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íť",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Žádné ověření",
"httpDestAuthNoneDescription": "Odešle žádosti bez záhlaví autorizace.",
"httpDestAuthBearerTitle": "Token na doručitele",
"httpDestAuthBearerDescription": "Přidává hlavičku Authorization: Bearer '<token>' k každému požadavku.",
"httpDestAuthBearerDescription": "Přidá autorizaci: Hlavička Bearer <token> ke každému požadavku.",
"httpDestAuthBearerPlaceholder": "Váš API klíč nebo token",
"httpDestAuthBasicTitle": "Základní ověření",
"httpDestAuthBasicDescription": "Přidává hlavičku Authorization: Basic '<credentials>'. Poskytněte přihlašovací údaje ve formátu uživatelské jméno:heslo.",
"httpDestAuthBasicDescription": "Přidá autorizaci: Základní <credentials> hlavička. Poskytněte přihlašovací údaje jako uživatelské jméno:password.",
"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-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.",
"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.",
"provisioningKeysBannerButtonText": "Mehr erfahren",
"pendingSitesBannerTitle": "Ausstehende Seiten",
"pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.",
"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.",
"pendingSitesBannerButtonText": "Mehr erfahren",
"apiKeysSettings": "{apiKeyName} Einstellungen",
"userTitle": "Alle Benutzer verwalten",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung."
}
},
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)",
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz keine Kasse)",
"buttons": {
"continueToCheckout": "Weiter zur Kasse"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Keine Authentifizierung",
"httpDestAuthNoneDescription": "Sendet Anfragen ohne Autorisierungs-Header.",
"httpDestAuthBearerTitle": "Bären-Token",
"httpDestAuthBearerDescription": "Fügt jedem Anfrage-Header eine \"Authorization: Bearer '<token>'\" hinzu.",
"httpDestAuthBearerDescription": "Fügt eine Berechtigung hinzu: Bearer <token> Header zu jeder Anfrage.",
"httpDestAuthBearerPlaceholder": "Ihr API-Schlüssel oder Token",
"httpDestAuthBasicTitle": "Einfacher Auth",
"httpDestAuthBasicDescription": "Fügt einen \"Authorization: Basic '<credentials>'\"-Header hinzu. Geben Sie die Anmeldedaten als Benutzername:Passwort an.",
"httpDestAuthBasicDescription": "Fügt eine Autorisierung hinzu: Basic <credentials> Kopfzeile hinzu. Geben Sie Anmeldedaten als Benutzername:password 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.",
"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.",
"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,9 +2609,6 @@
"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",
@@ -2850,10 +2847,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": "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.",
"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.",
"provisioningKeysBannerButtonText": "Saber más",
"pendingSitesBannerTitle": "Sitios pendientes",
"pendingSitesBannerDescription": "Los sitios que se conectan utilizando una clave de aprovisionamiento aparecen aquí para su revisión.",
"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.",
"pendingSitesBannerButtonText": "Saber más",
"apiKeysSettings": "Ajustes {apiKeyName}",
"userTitle": "Administrar todos los usuarios",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario."
}
},
"personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)",
"personalUseOnly": "Solo uso personal (licencia gratuita, sin pago)",
"buttons": {
"continueToCheckout": "Continuar con el pago"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Sin autenticación",
"httpDestAuthNoneDescription": "Envía solicitudes sin un encabezado de autorización.",
"httpDestAuthBearerTitle": "Tóken de portador",
"httpDestAuthBearerDescription": "Añade un encabezado Authorization: Bearer '<token>' a cada solicitud.",
"httpDestAuthBearerDescription": "Añade una autorización: portador <token> encabezado a cada solicitud.",
"httpDestAuthBearerPlaceholder": "Tu clave o token API",
"httpDestAuthBasicTitle": "Auth Básica",
"httpDestAuthBasicDescription": "Añade un encabezado Authorization: Basic '<credenciales>'. Proporcione las credenciales como nombredeusuario:contraseña.",
"httpDestAuthBasicDescription": "Añade una Autorización: encabezado básico <credentials> . Proporcione credenciales como nombre de usuario: 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 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.",
"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.",
"provisioningKeysBannerButtonText": "En savoir plus",
"pendingSitesBannerTitle": "Sites en attente",
"pendingSitesBannerDescription": "Les sites qui se connectent en utilisant une clé de provisionnement apparaissent ici pour révision.",
"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.",
"pendingSitesBannerButtonText": "En savoir plus",
"apiKeysSettings": "Paramètres de {apiKeyName}",
"userTitle": "Gérer tous les utilisateurs",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire."
}
},
"personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)",
"personalUseOnly": "Utilisation personnelle uniquement (licence gratuite — sans checkout)",
"buttons": {
"continueToCheckout": "Continuer vers le paiement"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,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 un en-tête Authorization: Basic '<credentials>'. Fournissez les identifiants sous la forme nom d'utilisateur:mot de passe.",
"httpDestAuthBasicDescription": "Ajoute une autorisation : en-tête de base <credentials> . Fournissez des informations d'identification comme 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": "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.",
"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.",
"provisioningKeysBannerButtonText": "Scopri di più",
"pendingSitesBannerTitle": "Siti In Attesa",
"pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning vengono visualizzati qui per la revisione.",
"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.",
"pendingSitesBannerButtonText": "Scopri di più",
"apiKeysSettings": "Impostazioni {apiKeyName}",
"userTitle": "Gestisci Tutti Gli Utenti",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario."
}
},
"personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)",
"personalUseOnly": "Solo uso personale (licenza gratuita nessun checkout)",
"buttons": {
"continueToCheckout": "Continua al Checkout"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Nessuna Autenticazione",
"httpDestAuthNoneDescription": "Invia richieste senza intestazione autorizzazione.",
"httpDestAuthBearerTitle": "Token Del Portatore",
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Authorization: Bearer '<token>' a ogni richiesta.",
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Autorizzazione: Bearer <token> ad ogni richiesta.",
"httpDestAuthBearerPlaceholder": "La tua chiave API o token",
"httpDestAuthBasicTitle": "Autenticazione Base",
"httpDestAuthBasicDescription": "Aggiunge un'intestazione Authorization: Basic '<credentials>'. Fornire le credenziali come username:password.",
"httpDestAuthBasicDescription": "Aggiunge un'autorizzazione: intestazione di base <credentials> . Fornisce 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,8 +624,6 @@
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
"targetErrorNoSite": "선택된 사이트 없음",
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
"targetTargetsCleared": "대상이 제거됨",
"targetTargetsClearedDescription": "이 리소스에서 모든 대상이 제거되었습니다",
"targetCreated": "대상 생성",
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
"targetErrorCreate": "대상 생성 실패",
@@ -2348,7 +2346,7 @@
"description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원."
}
},
"personalUseOnly": "개인용으로만 사용 (무료 라이- 결제 없음)",
"personalUseOnly": "개인 사용 전용 (무료 라이— 체크아웃 없음)",
"buttons": {
"continueToCheckout": "결제로 진행"
},
@@ -2609,9 +2607,6 @@
"machineClients": "기계 클라이언트",
"install": "설치",
"run": "실행",
"envFile": "환경 파일",
"serviceFile": "서비스 파일",
"enableAndStart": "활성화 및 시작",
"clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.",
"clientAddress": "클라이언트 주소(고급)",
"setupFailedToFetchSubnet": "기본값 로드 실패",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "인증 없음",
"httpDestAuthNoneDescription": "Authorization 헤더 없이 요청을 보냅니다.",
"httpDestAuthBearerTitle": "Bearer 토큰",
"httpDestAuthBearerDescription": " 요청에 Authorization: Bearer '<token>' 헤더를 추가합니다.",
"httpDestAuthBearerDescription": "모든 요청에 Authorization: Bearer <token> 헤더를 추가합니다.",
"httpDestAuthBearerPlaceholder": "API 키 또는 토큰",
"httpDestAuthBasicTitle": "기본 인증",
"httpDestAuthBasicDescription": "Authorization: Basic '<credentials>' 헤더를 추가합니다. 자격 증명은 사용자 이름:비밀번호로 제공합니다.",
"httpDestAuthBasicDescription": "Authorization: Basic <credentials> 헤더를 추가합니다. 자격 증명은 username:password 형식으로 제공하세요.",
"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 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.",
"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.",
"provisioningKeysBannerButtonText": "Lær mer",
"pendingSitesBannerTitle": "Ventende nettsteder",
"pendingSitesBannerDescription": "Nettsteder som kobler seg til ved bruk av en provisjonsnøkkel vises her for vurdering.",
"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.",
"pendingSitesBannerButtonText": "Lær mer",
"apiKeysSettings": "{apiKeyName} Innstillinger",
"userTitle": "Administrer alle brukere",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte."
}
},
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)",
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)",
"buttons": {
"continueToCheckout": "Fortsett til kassen"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,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 Autorisasjon: Basic '<credentials>' header. Gi legitimasjon som brukernavn:passord.",
"httpDestAuthBasicDescription": "Legger til en godkjenning: Grunnleggende <credentials> overskrift. Angi 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 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.",
"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.",
"provisioningKeysBannerButtonText": "Meer informatie",
"pendingSitesBannerTitle": "Openstaande sites",
"pendingSitesBannerDescription": "Sites die verbinding maken met een inrichtingssleutel verschijnen hier voor beoordeling.",
"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.",
"pendingSitesBannerButtonText": "Meer informatie",
"apiKeysSettings": "{apiKeyName} instellingen",
"userTitle": "Alle gebruikers beheren",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning."
}
},
"personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)",
"personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)",
"buttons": {
"continueToCheckout": "Doorgaan naar afrekenen"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Geen authenticatie",
"httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.",
"httpDestAuthBearerTitle": "Betere Token",
"httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '<token>' header toe aan elk verzoek.",
"httpDestAuthBearerDescription": "Voegt een machtiging toe: Drager <token> header aan elke aanvraag.",
"httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token",
"httpDestAuthBasicTitle": "Basis authenticatie",
"httpDestAuthBasicDescription": "Voegt een Authorization: Basic '<credentials>' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.",
"httpDestAuthBasicDescription": "Voegt een Authorizatie toe: Basis <credentials> kop. Geef inloggegevens op 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 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.",
"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.",
"provisioningKeysBannerButtonText": "Dowiedz się więcej",
"pendingSitesBannerTitle": "Witryny oczekujące",
"pendingSitesBannerDescription": "Witryny, które łączą się za pomocą klucza provisioning, pojawią się tutaj do przeglądu.",
"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.",
"pendingSitesBannerButtonText": "Dowiedz się więcej",
"apiKeysSettings": "Ustawienia {apiKeyName}",
"userTitle": "Zarządzaj wszystkimi użytkownikami",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe."
}
},
"personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)",
"personalUseOnly": "Wyłącznie do użytku osobistego (bezpłatna licencja brak zamówień)",
"buttons": {
"continueToCheckout": "Przejdź do zamówienia"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Brak uwierzytelniania",
"httpDestAuthNoneDescription": "Wysyła żądania bez nagłówka autoryzacji.",
"httpDestAuthBearerTitle": "Token Bearer",
"httpDestAuthBearerDescription": "Dodaje nagłówek Authorization: Bearer '<token>' do każdego żądania.",
"httpDestAuthBearerDescription": "Dodaje autoryzację: nagłówek Bearer <token> do każdego żądania.",
"httpDestAuthBearerPlaceholder": "Twój klucz API lub token",
"httpDestAuthBasicTitle": "Podstawowa Autoryzacja",
"httpDestAuthBasicDescription": "Dodaje nagłówek Authorization: Basic '<credentials>'. Podaj poświadczenia w formacie użytkownik:hasło.",
"httpDestAuthBasicDescription": "Dodaje Autoryzacja: Nagłówek Basic <credentials> . Podaj poświadczenia jako nazwę użytkownika: 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": "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.",
"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.",
"provisioningKeysBannerButtonText": "Saiba mais",
"pendingSitesBannerTitle": "Sites pendentes",
"pendingSitesBannerDescription": "Sites que se conectam usando uma chave de provisionamento aparecem aqui para revisão.",
"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.",
"pendingSitesBannerButtonText": "Saiba mais",
"apiKeysSettings": "Configurações de {apiKeyName}",
"userTitle": "Gerir Todos os Utilizadores",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário."
}
},
"personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)",
"personalUseOnly": "Apenas uso pessoal (licença gratuita sem check-out)",
"buttons": {
"continueToCheckout": "Continuar com checkout"
},
@@ -2609,9 +2607,6 @@
"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",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Sem Autenticação",
"httpDestAuthNoneDescription": "Envia pedidos sem um cabeçalho de autorização.",
"httpDestAuthBearerTitle": "Token do portador",
"httpDestAuthBearerDescription": "Adiciona um cabeçalho Authorization: Bearer '<token>' a cada solicitação.",
"httpDestAuthBearerDescription": "Adiciona uma autorização: Bearer <token> header a cada requisição.",
"httpDestAuthBearerPlaceholder": "Sua chave de API ou token",
"httpDestAuthBasicTitle": "Autenticação básica",
"httpDestAuthBasicDescription": "Adiciona um cabeçalho Authorization: Basic '<credentials>'. Forneça as credenciais como username:password.",
"httpDestAuthBasicDescription": "Adiciona uma Autorização: cabeçalho <credentials> básico. Forneça credenciais como nome de usuário:senha.",
"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": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
"provisioningKeysBannerDescription": "Генерировать подготовительный ключ и использовать его вместе с Новым коннектором для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
"provisioningKeysBannerButtonText": "Узнать больше",
"pendingSitesBannerTitle": "Ожидающие сайты",
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
"pendingSitesBannerDescription": "Сайты, связанные с использованием ключа подготовки, появляются здесь для проверки. Одобрите каждый сайт, прежде чем он станет активным и получит доступ к вашим ресурсам.",
"pendingSitesBannerButtonText": "Узнать больше",
"apiKeysSettings": "Настройки {apiKeyName}",
"userTitle": "Управление всеми пользователями",
@@ -624,8 +624,6 @@
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
"targetErrorNoSite": "Сайт не выбран",
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
"targetTargetsCleared": "Цели очищены",
"targetTargetsClearedDescription": "Все цели удалены из этого ресурса",
"targetCreated": "Цель создана",
"targetCreatedDescription": "Цель была успешно создана",
"targetErrorCreate": "Не удалось создать цель",
@@ -2348,7 +2346,7 @@
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка."
}
},
"personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)",
"personalUseOnly": "Только для личного пользования (бесплатная лицензия без оформления)",
"buttons": {
"continueToCheckout": "Продолжить оформление заказа"
},
@@ -2609,9 +2607,6 @@
"machineClients": "Машинные клиенты",
"install": "Установить",
"run": "Запустить",
"envFile": "Файл окружения",
"serviceFile": "Сервисный файл",
"enableAndStart": "Включить и запустить",
"clientNameDescription": "Отображаемое имя клиента, которое может быть изменено позже.",
"clientAddress": "Адрес клиента (Дополнительно)",
"setupFailedToFetchSubnet": "Не удалось получить подсеть по умолчанию",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "Нет аутентификации",
"httpDestAuthNoneDescription": "Отправляет запросы без заголовка авторизации.",
"httpDestAuthBearerTitle": "Жетон носителя",
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer '<token>' к каждому запросу.",
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer <token> к каждому запросу.",
"httpDestAuthBearerPlaceholder": "Ваш ключ API или токен",
"httpDestAuthBasicTitle": "Базовая авторизация",
"httpDestAuthBasicDescription": "Добавляет заголовок Authorization: Basic '<credentials>'. Укажите учетные данные в формате username:password.",
"httpDestAuthBasicDescription": "Добавляет Authorization: Basic <credentials> header. Предоставьте учетные данные в качестве имени пользователя: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": "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.",
"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.",
"provisioningKeysBannerButtonText": "Daha fazla bilgi",
"pendingSitesBannerTitle": "Bekleyen Siteler",
"pendingSitesBannerDescription": "Bir sağlama anahtarı kullanarak bağlanan siteler, inceleme için burada görünür.",
"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.",
"pendingSitesBannerButtonText": "Daha fazla bilgi",
"apiKeysSettings": "{apiKeyName} Ayarları",
"userTitle": "Tüm Kullanıcıları Yönet",
@@ -624,8 +624,6 @@
"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",
@@ -2348,7 +2346,7 @@
"description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek."
}
},
"personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)",
"personalUseOnly": "Yalnızca kişisel kullanım (ücretsiz lisans ödeme yapılmaz)",
"buttons": {
"continueToCheckout": "Ödemeye Devam Et"
},
@@ -2609,9 +2607,6 @@
"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ı",
@@ -2850,10 +2845,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>' üst bilgisi ekler.",
"httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı <token> başlığı ekler.",
"httpDestAuthBearerPlaceholder": "API anahtarınız veya jetonunuz",
"httpDestAuthBasicTitle": "Temel Kimlik Doğrulama",
"httpDestAuthBasicDescription": "Bir Yetkilendirme: Temel '<credentials>' üst bilgisi ekler. Kimlik bilgilerini kullanıcı adı:şifre olarak sağlayın.",
"httpDestAuthBasicDescription": "Authorization: Temel <belirtecikler> başlığı ekler. Yetkilendirmeleri 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": "生成一个供应密钥,并将其与 Newt 连接器一起使用,以在首次启动时自动创建站点 - 无需为每个站点设置单独的凭。",
"provisioningKeysBannerDescription": "生成一个预配键并使用它来在首次启动时自动创建站点——无需为每个站点设置单独的凭。",
"provisioningKeysBannerButtonText": "了解更多",
"pendingSitesBannerTitle": "待定站点",
"pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。",
"pendingSitesBannerDescription": "使用预配键连接的站点会出现在这里供审核。在站点开始运行之前批准并获取对您资源的访问权限。",
"pendingSitesBannerButtonText": "了解更多",
"apiKeysSettings": "{apiKeyName} 设置",
"userTitle": "管理所有用户",
@@ -624,8 +624,6 @@
"targetErrorInvalidPortDescription": "请输入有效的端口号",
"targetErrorNoSite": "没有选择站点",
"targetErrorNoSiteDescription": "请选择目标站点",
"targetTargetsCleared": "目标已清除",
"targetTargetsClearedDescription": "所有目标已从此资源中移除",
"targetCreated": "目标已创建",
"targetCreatedDescription": "目标已成功创建",
"targetErrorCreate": "创建目标失败",
@@ -2348,7 +2346,7 @@
"description": "企业特征、50个用户、50个站点和优先支持。"
}
},
"personalUseOnly": "仅个人使用免费许可 - 无需结账)",
"personalUseOnly": "仅个人使用 (免费许可证-无签出)",
"buttons": {
"continueToCheckout": "继续签出"
},
@@ -2609,9 +2607,6 @@
"machineClients": "机器客户端",
"install": "安装",
"run": "运行",
"envFile": "环境文件",
"serviceFile": "服务文件",
"enableAndStart": "启用并启动",
"clientNameDescription": "可以稍后更改的客户端的显示名称。",
"clientAddress": "客户端地址 (高级)",
"setupFailedToFetchSubnet": "获取默认子网失败",
@@ -2850,10 +2845,10 @@
"httpDestAuthNoneTitle": "无身份验证",
"httpDestAuthNoneDescription": "在没有授权头的情况下发送请求。",
"httpDestAuthBearerTitle": "持有者令牌",
"httpDestAuthBearerDescription": "在每个请求中添加授权Bearer “<token>” 头。",
"httpDestAuthBearerDescription": "添加授权:每个请求的标题为 <token>。",
"httpDestAuthBearerPlaceholder": "您的 API 密钥或令牌",
"httpDestAuthBasicTitle": "基本认证",
"httpDestAuthBasicDescription": "添加一个Authorization: Basic \"<凭据>\" 标头。 以用户名:密码形式提供凭据。",
"httpDestAuthBasicDescription": "添加授权:基本 <credentials> 头。提供用户名:密码凭据。",
"httpDestAuthBasicPlaceholder": "用户名:密码",
"httpDestAuthCustomTitle": "自定义标题",
"httpDestAuthCustomDescription": "指定自定义 HTTP 头名称和身份验证值 (例如X-API 键)。",

157
package-lock.json generated
View File

@@ -36,9 +36,9 @@
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.8",
"@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.5",
"@react-email/components": "1.0.11",
"@react-email/render": "2.0.5",
"@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.0",
"@tailwindcss/forms": "0.5.11",
@@ -55,19 +55,19 @@
"cors": "2.8.6",
"crypto-js": "4.2.0",
"d3": "7.9.0",
"drizzle-orm": "0.45.1",
"drizzle-orm": "0.45.2",
"express": "5.2.1",
"express-rate-limit": "8.3.0",
"express-rate-limit": "8.3.2",
"glob": "13.0.6",
"helmet": "8.1.0",
"http-errors": "2.0.1",
"input-otp": "1.4.2",
"ioredis": "5.10.0",
"ioredis": "5.10.1",
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.577.0",
"maxmind": "5.0.5",
"maxmind": "5.0.6",
"moment": "2.30.1",
"next": "15.5.14",
"next-intl": "4.8.3",
@@ -77,7 +77,7 @@
"nodemailer": "8.0.4",
"oslo": "1.2.1",
"pg": "8.20.0",
"posthog-node": "5.28.0",
"posthog-node": "5.28.10",
"qrcode.react": "4.2.0",
"react": "19.2.4",
"react-day-picker": "9.14.0",
@@ -89,13 +89,13 @@
"reodotdev": "1.1.0",
"resend": "6.9.2",
"semver": "7.7.4",
"sshpk": "^1.18.0",
"sshpk": "1.18.0",
"stripe": "20.4.1",
"swagger-ui-express": "5.0.1",
"tailwind-merge": "3.5.0",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0",
"use-debounce": "^10.1.0",
"use-debounce": "10.1.1",
"uuid": "13.0.0",
"vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0",
@@ -130,7 +130,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"@types/sshpk": "^1.17.4",
"@types/sshpk": "1.17.4",
"@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1",
@@ -4143,13 +4143,10 @@
}
},
"node_modules/@posthog/core": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.2.tgz",
"integrity": "sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
"version": "1.24.5",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.5.tgz",
"integrity": "sha512-umXx3kMjM+cTUTLDsdPFFU7aJa3uiH19EEoWKbE5QVME8WgVg7q1peMhK7y7n7xRmYJlA70eOrHQfWlzBQqeFQ==",
"license": "MIT"
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
@@ -6386,18 +6383,6 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-email/body": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.1.tgz",
"integrity": "sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@react-email/button": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.1.tgz",
@@ -6450,12 +6435,12 @@
}
},
"node_modules/@react-email/components": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz",
"integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.11.tgz",
"integrity": "sha512-s0CX31+S/u1MhBWYFAuZru0NHNExTY+OeZC9OrGyzl8PGQ0Iz/4gq3O4rHUVuA1D7FjAcPbwG1Up0yey/Xh6dw==",
"license": "MIT",
"dependencies": {
"@react-email/body": "0.2.1",
"@react-email/body": "0.3.0",
"@react-email/button": "0.2.1",
"@react-email/code-block": "0.2.1",
"@react-email/code-inline": "0.0.6",
@@ -6470,10 +6455,10 @@
"@react-email/link": "0.0.13",
"@react-email/markdown": "0.0.18",
"@react-email/preview": "0.0.14",
"@react-email/render": "2.0.4",
"@react-email/render": "2.0.5",
"@react-email/row": "0.0.13",
"@react-email/section": "0.0.17",
"@react-email/tailwind": "2.0.5",
"@react-email/tailwind": "2.0.7",
"@react-email/text": "0.1.6"
},
"engines": {
@@ -6483,6 +6468,18 @@
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@react-email/components/node_modules/@react-email/body": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.3.0.tgz",
"integrity": "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@react-email/container": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz",
@@ -6854,9 +6851,9 @@
}
},
"node_modules/@react-email/render": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz",
"integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.5.tgz",
"integrity": "sha512-oAsSpY/vYt9ReDcRQDBLxENwCNAklkE6bvP5Kl9ZlmVr/RZpfhloJp8xc/OZki/YF2nisRRX50aEy8P9v3R5GA==",
"license": "MIT",
"dependencies": {
"html-to-text": "^9.0.5",
@@ -6895,9 +6892,9 @@
}
},
"node_modules/@react-email/tailwind": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz",
"integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.7.tgz",
"integrity": "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==",
"license": "MIT",
"dependencies": {
"tailwindcss": "^4.1.18"
@@ -6906,17 +6903,17 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"@react-email/body": "0.2.1",
"@react-email/button": "0.2.1",
"@react-email/code-block": "0.2.1",
"@react-email/code-inline": "0.0.6",
"@react-email/container": "0.0.16",
"@react-email/heading": "0.0.16",
"@react-email/hr": "0.0.12",
"@react-email/img": "0.0.12",
"@react-email/link": "0.0.13",
"@react-email/preview": "0.0.14",
"@react-email/text": "0.1.6",
"@react-email/body": ">=0",
"@react-email/button": ">=0",
"@react-email/code-block": ">=0",
"@react-email/code-inline": ">=0",
"@react-email/container": ">=0",
"@react-email/heading": ">=0",
"@react-email/hr": ">=0",
"@react-email/img": ">=0",
"@react-email/link": ">=0",
"@react-email/preview": ">=0",
"@react-email/text": ">=0",
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
@@ -10854,6 +10851,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -10868,12 +10866,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/cross-spawn/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -11731,9 +11731,9 @@
}
},
"node_modules/drizzle-orm": {
"version": "0.45.1",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz",
"integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==",
"version": "0.45.2",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
"integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
@@ -12951,9 +12951,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz",
"integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==",
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
@@ -13950,9 +13950,9 @@
}
},
"node_modules/ioredis": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz",
"integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.5.1",
@@ -15098,13 +15098,13 @@
}
},
"node_modules/maxmind": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.6.tgz",
"integrity": "sha512-5bvd/u+kIaTqaGM+xkXjatzQw1dQfSmlLggr2W1EKMyMxSgx2woZyusLpNpZ4DdPmL+1bbJWeo4LXsi6bC0Iew==",
"license": "MIT",
"dependencies": {
"mmdb-lib": "3.0.2",
"tiny-lru": "11.4.7"
"tiny-lru": "13.0.0"
},
"engines": {
"node": ">=12",
@@ -16310,6 +16310,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -16607,12 +16608,12 @@
}
},
"node_modules/posthog-node": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.28.0.tgz",
"integrity": "sha512-EETYV0zA+7BLQmXzY+vGyDMoQK8uHf8f/1utbRjKncI41gPkw+4piGP7l4UT5Luld+4vQpJPOR1q1YrbXm7XjQ==",
"version": "5.28.10",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.28.10.tgz",
"integrity": "sha512-FxZFnX/3a7Edc2VIpiyne3BiqmzZRQv5nAItaO1urZDhC5fPWZeeo04aeKl0LWKBUp7M7x6a2sQYHpqVztT4GQ==",
"license": "MIT",
"dependencies": {
"@posthog/core": "1.23.2"
"@posthog/core": "1.24.5"
},
"engines": {
"node": "^20.20.0 || >=22.22.0"
@@ -17881,6 +17882,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -17893,6 +17895,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -18733,12 +18736,12 @@
"license": "MIT"
},
"node_modules/tiny-lru": {
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-13.0.0.tgz",
"integrity": "sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
"node": ">=14"
}
},
"node_modules/tinyexec": {
@@ -19329,9 +19332,9 @@
}
},
"node_modules/use-debounce": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz",
"integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.1.tgz",
"integrity": "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==",
"license": "MIT",
"engines": {
"node": ">= 16.0.0"

View File

@@ -59,9 +59,9 @@
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.8",
"@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.5",
"@react-email/components": "1.0.11",
"@react-email/render": "2.0.5",
"@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.0",
"@tailwindcss/forms": "0.5.11",
@@ -78,19 +78,19 @@
"cors": "2.8.6",
"crypto-js": "4.2.0",
"d3": "7.9.0",
"drizzle-orm": "0.45.1",
"drizzle-orm": "0.45.2",
"express": "5.2.1",
"express-rate-limit": "8.3.0",
"express-rate-limit": "8.3.2",
"glob": "13.0.6",
"helmet": "8.1.0",
"http-errors": "2.0.1",
"input-otp": "1.4.2",
"ioredis": "5.10.0",
"ioredis": "5.10.1",
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.577.0",
"maxmind": "5.0.5",
"maxmind": "5.0.6",
"moment": "2.30.1",
"next": "15.5.14",
"next-intl": "4.8.3",
@@ -100,7 +100,7 @@
"nodemailer": "8.0.4",
"oslo": "1.2.1",
"pg": "8.20.0",
"posthog-node": "5.28.0",
"posthog-node": "5.28.10",
"qrcode.react": "4.2.0",
"react": "19.2.4",
"react-day-picker": "9.14.0",
@@ -118,7 +118,7 @@
"tailwind-merge": "3.5.0",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0",
"use-debounce": "10.1.0",
"use-debounce": "10.1.1",
"uuid": "13.0.0",
"vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0",

View File

@@ -479,7 +479,10 @@ export async function getTraefikConfig(
// TODO: HOW TO HANDLE ^^^^^^ BETTER
const anySitesOnline = targets.some(
(target) => target.site.online
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
);
return (
@@ -492,7 +495,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;
@@ -607,7 +610,10 @@ export async function getTraefikConfig(
servers: (() => {
// Check if any sites are online
const anySitesOnline = targets.some(
(target) => target.site.online
(target) =>
target.site.online ||
target.site.type === "local" ||
target.site.type === "wireguard"
);
return targets
@@ -615,7 +621,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,8 +23,6 @@ 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,
@@ -129,7 +127,7 @@ export class LogStreamingManager {
start(): void {
if (this.isRunning) return;
this.isRunning = true;
logger.debug("LogStreamingManager: started");
logger.info("LogStreamingManager: started");
this.schedulePoll(POLL_INTERVAL_MS);
}
@@ -274,20 +272,19 @@ export class LogStreamingManager {
return;
}
// Decrypt and parse config skip destination if either step fails
let configFromDb: HttpConfig;
// Parse config skip destination if config is unparseable
let config: HttpConfig;
try {
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
config = JSON.parse(dest.config) as HttpConfig;
} catch (err) {
logger.error(
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
`LogStreamingManager: destination ${dest.destinationId} has invalid JSON config`,
err
);
return;
}
const provider = this.createProvider(dest.type, configFromDb);
const provider = this.createProvider(dest.type, config);
if (!provider) {
logger.warn(
`LogStreamingManager: unsupported destination type "${dest.type}" ` +
@@ -773,4 +770,4 @@ export class LogStreamingManager {
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

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

View File

@@ -22,8 +22,6 @@ 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()
@@ -89,10 +87,7 @@ export async function createEventStreamingDestination(
);
}
const { type, config: configToSet, enabled } = parsedBody.data;
const key = config.getRawConfig().server.secret!;
const encryptedConfig = encrypt(configToSet, key);
const { type, config, enabled } = parsedBody.data;
const now = Date.now();
@@ -101,7 +96,7 @@ export async function createEventStreamingDestination(
.values({
orgId,
type,
config: encryptedConfig,
config,
enabled,
createdAt: now,
updatedAt: now,

View File

@@ -22,8 +22,6 @@ 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()
@@ -123,22 +121,9 @@ 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: decryptedList,
destinations: list,
pagination: {
total: count,
limit,

View File

@@ -22,8 +22,7 @@ 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({
@@ -111,17 +110,14 @@ export async function updateEventStreamingDestination(
);
}
const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
const { type, config, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
const updateData: Record<string, unknown> = {
updatedAt: Date.now()
};
if (type !== undefined) updateData.type = type;
if (configToUpdate !== undefined) {
const key = config.getRawConfig().server.secret!;
updateData.config = encrypt(configToUpdate, key);
}
if (config !== undefined) updateData.config = config;
if (enabled !== undefined) updateData.enabled = enabled;
if (sendAccessLogs !== undefined) updateData.sendAccessLogs = sendAccessLogs;
if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;

View File

@@ -171,8 +171,9 @@ 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}::text, ${bytesIn}::real, ${bytesOut}::real)`
const valuesList = chunk.map(
([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}, ${bytesIn}, ${bytesOut})`
);
const valuesClause = sql.join(valuesList, sql`, `);
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`

View File

@@ -8,7 +8,6 @@ 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;
@@ -56,7 +55,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
logger.warn(
`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}?`
`handleGetConfigMessage: Site ${existingSite.siteId} last hole punch is too old, skipping`
);
return;
}

View File

@@ -1,4 +1,4 @@
import { db, newts, sites, targetHealthCheck, targets } from "@server/db";
import { db, newts, sites } from "@server/db";
import {
hasActiveConnections,
getClientConfigVersion
@@ -78,32 +78,6 @@ 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
@@ -128,8 +102,7 @@ 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 { inArray } from "drizzle-orm";
import { eq, 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 = 5;
const MAX_RETRIES = 2;
const BASE_DELAY_MS = 50;
// ── Site (newt) pings ──────────────────────────────────────────────────
@@ -36,14 +36,6 @@ 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 ─────────────────────────────────────────────────────────
/**
@@ -80,12 +72,6 @@ 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) {
@@ -97,35 +83,55 @@ async function flushSitePingsToDb(): Promise<void> {
const pingsToFlush = new Map(pendingSitePings);
pendingSitePings.clear();
const entries = Array.from(pingsToFlush.entries());
// Sort by siteId for consistent lock ordering (prevents deadlocks)
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
([a], [b]) => a - b
);
const BATCH_SIZE = 50;
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);
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
try {
await withRetry(async () => {
await db
.update(sites)
.set({
online: true,
lastPing: maxTimestamp
})
.where(inArray(sites.siteId, siteIds));
// 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));
}
});
}
}, "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) {
@@ -138,8 +144,6 @@ 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) {
@@ -155,25 +159,51 @@ async function flushClientPingsToDb(): Promise<void> {
// ── Flush client pings ─────────────────────────────────────────────
if (pingsToFlush.size > 0) {
const entries = Array.from(pingsToFlush.entries());
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
([a], [b]) => a - b
);
const BATCH_SIZE = 50;
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);
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
try {
await withRetry(async () => {
await db
.update(clients)
.set({
lastPing: maxTimestamp,
online: true,
archived: false
})
.where(inArray(clients.clientId, clientIds));
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)
);
}
});
}
}, "flushClientPingsToDb");
} catch (error) {
logger.error(
@@ -230,12 +260,7 @@ export async function flushPingsToDb(): Promise<void> {
/**
* Simple retry wrapper with exponential backoff for transient errors
* (deadlocks, connection timeouts, unexpected disconnects).
*
* PostgreSQL deadlocks (40P01) are always safe to retry: the database
* guarantees exactly one winner per deadlock pair, so the loser just needs
* to try again. MAX_RETRIES is intentionally higher than typical connection
* retry budgets to give deadlock victims enough chances to succeed.
* (connection timeouts, unexpected disconnects).
*/
async function withRetry<T>(
operation: () => Promise<T>,
@@ -252,8 +277,7 @@ 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`,
{ code: error?.code ?? error?.cause?.code }
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
@@ -264,14 +288,14 @@ async function withRetry<T>(
}
/**
* Detect transient errors that are safe to retry.
* Detect transient connection errors that are safe to retry.
*/
function isTransientError(error: any): boolean {
if (!error) return false;
const message = (error.message || "").toLowerCase();
const causeMessage = (error.cause?.message || "").toLowerCase();
const code = error.code || error.cause?.code || "";
const code = error.code || "";
// Connection timeout / terminated
if (
@@ -284,17 +308,12 @@ function isTransientError(error: any): boolean {
return true;
}
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
// PostgreSQL deadlock
if (code === "40P01" || message.includes("deadlock")) {
return true;
}
// PostgreSQL serialization failure
if (code === "40001") {
return true;
}
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
// ECONNRESET, ECONNREFUSED, EPIPE
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
@@ -318,26 +337,12 @@ 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);
@@ -359,22 +364,7 @@ export async function stopPingAccumulator(): Promise<void> {
flushTimer = null;
}
// 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);
});
}
// Final flush to persist any remaining pings
try {
await flushPingsToDb();
} catch (error) {
@@ -389,4 +379,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 { getNextAvailableClientSubnet } from "@server/lib/ip";
import { v } from "@faker-js/faker/dist/airline-Dz1uGqgJ";
const bodySchema = z.object({
provisioningKey: z.string().nonempty(),
@@ -152,11 +152,6 @@ 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") {
@@ -195,20 +190,6 @@ 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)
@@ -216,7 +197,6 @@ export async function registerNewt(
orgId,
name: name || niceId,
niceId,
address: clientAddress,
type: "newt",
dockerSocketEnabled: true,
status: keyRecord.approveNewSites ? "approved" : "pending",

View File

@@ -20,7 +20,6 @@ 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!");
@@ -275,7 +274,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. 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}?`
"Client last hole punch is too old and we have sites to send; skipping this register"
);
return;
}

View File

@@ -77,8 +77,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
const [targetCheck] = await db
.select({
targetId: targets.targetId,
siteId: targets.siteId,
hcStatus: targetHealthCheck.hcHealth
siteId: targets.siteId
})
.from(targets)
.innerJoin(
@@ -86,7 +85,6 @@ 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),
@@ -103,14 +101,6 @@ 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,42 +104,6 @@ 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";`
);
@@ -213,12 +177,8 @@ 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,15 +76,9 @@ 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(
`
@@ -174,42 +168,6 @@ 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();
@@ -233,12 +191,8 @@ 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.userId ? "user" : "machine";
const clientType = row.original.clientType === "olm" ? "machine" : "user";
if (row.original.clientName && row.original.clientNiceId) {
return (
<Link

View File

@@ -106,9 +106,7 @@ 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>
@@ -162,9 +160,7 @@ 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>
);
@@ -190,9 +186,7 @@ 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"),
@@ -239,19 +233,13 @@ 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}
@@ -313,7 +301,10 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToLoad"),
description: formatAxiosError(e, t("streamingUnexpectedError"))
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
});
} finally {
setLoading(false);
@@ -350,7 +341,10 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToUpdate"),
description: formatAxiosError(e, t("streamingUnexpectedError"))
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
});
} finally {
setTogglingIds((prev) => {
@@ -381,7 +375,10 @@ export default function StreamingDestinationsPage() {
toast({
variant: "destructive",
title: t("streamingFailedToDelete"),
description: formatAxiosError(e, t("streamingUnexpectedError"))
description: formatAxiosError(
e,
t("streamingUnexpectedError")
)
});
} finally {
setDeleting(false);
@@ -462,14 +459,13 @@ 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>
<p className="text-sm text-muted-foreground">
{t("streamingDeleteDialogAreYouSure")}{" "}
<span>
<span className="font-semibold text-foreground">
{parseHttpConfig(deleteTarget.config).name ||
t("streamingDeleteDialogThisDestination")}
</span>
@@ -482,4 +478,4 @@ export default function StreamingDestinationsPage() {
)}
</>
);
}
}

View File

@@ -9,8 +9,6 @@ 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 }>;
@@ -98,10 +96,6 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) {
</Button>
</Link>
</DismissableBanner>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.SiteProvisioningKeys]}
/>
<PendingSitesTable
sites={siteRows}
orgId={params.orgId}

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,6 @@ 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,
@@ -65,10 +63,6 @@ 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"])
@@ -456,7 +450,6 @@ export default function PendingSitesTable({
onSearch={handleSearchChange}
onRefresh={refreshData}
isRefreshing={isRefreshing || isFiltering}
refreshButtonDisabled={!canUseSiteProvisioning}
rowCount={rowCount}
columnVisibility={{
niceId: false,

View File

@@ -311,7 +311,6 @@ 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 { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
import { SiKubernetes, SiNixos } from "react-icons/si";
export type CommandItem = string | { title: string; command: string };
const PLATFORMS = [
"linux",
"macos",
"unix",
"docker",
"kubernetes",
"podman",
@@ -43,7 +43,7 @@ export function NewtSiteInstallCommands({
const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true);
const [platform, setPlatform] = useState<Platform>("linux");
const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
@@ -54,68 +54,8 @@ export function NewtSiteInstallCommands({
: "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
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: [
unix: {
All: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
@@ -191,7 +131,7 @@ WantedBy=default.target`
]
},
nixos: {
Flake: [
All: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
}
@@ -232,9 +172,9 @@ WantedBy=default.target`
<OptionSelect<string>
label={
platform === "windows"
? t("architecture")
: t("method")
["docker", "podman"].includes(platform)
? t("method")
: t("architecture")
}
options={getArchitectures(platform).map((arch) => ({
value: arch,
@@ -321,10 +261,8 @@ function getPlatformIcon(platformName: Platform) {
switch (platformName) {
case "windows":
return <FaWindows 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 "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
@@ -334,7 +272,7 @@ function getPlatformIcon(platformName: Platform) {
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
default:
return <FaLinux className="h-4 w-4 mr-2" />;
return <Terminal className="h-4 w-4 mr-2" />;
}
}
@@ -342,10 +280,8 @@ function getPlatformName(platformName: Platform) {
switch (platformName) {
case "windows":
return "Windows";
case "linux":
return "Linux";
case "macos":
return "macOS";
case "unix":
return "Unix & macOS";
case "docker":
return "Docker";
case "kubernetes":
@@ -355,16 +291,14 @@ function getPlatformName(platformName: Platform) {
case "nixos":
return "NixOS";
default:
return "Linux";
return "Unix / macOS";
}
}
function getArchitectures(platform: Platform) {
switch (platform) {
case "linux":
return ["Run", "Systemd Service"];
case "macos":
return ["Run"];
case "unix":
return ["All"];
case "windows":
return ["x64"];
case "docker":
@@ -374,8 +308,8 @@ function getArchitectures(platform: Platform) {
case "podman":
return ["Podman Quadlet", "Podman Run"];
case "nixos":
return ["Flake"];
return ["All"];
default:
return ["Run"];
return ["x64"];
}
}

View File

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

View File

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

View File

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

View File

@@ -22,21 +22,12 @@ 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)) {
// 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.
}
// 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"
});
return userLocale as Locale;
}
} catch {