mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-09 17:18:31 +00:00
Add resource column to hc and remove —
This commit is contained in:
parent
b958537f3e
commit
d6c15c8b81
60 changed files with 257 additions and 211 deletions
|
|
@ -53,9 +53,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
||||||
|
|
||||||
## Deployment Options
|
## Deployment Options
|
||||||
|
|
||||||
- **Pangolin Cloud** — Fully managed service - no infrastructure required.
|
- **Pangolin Cloud** - Fully managed service - no infrastructure required.
|
||||||
- **Self-Host: Community Edition** — Free, open source, and licensed under AGPL-3.
|
- **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 making less than \$100K USD gross annual revenue.
|
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ def process_directory(root_dir):
|
||||||
if has_correct_header:
|
if has_correct_header:
|
||||||
print(f"Header up-to-date: {file_path}")
|
print(f"Header up-to-date: {file_path}")
|
||||||
else:
|
else:
|
||||||
# Either no header exists or the header is outdated — write
|
# Either no header exists or the header is outdated - write
|
||||||
# the correct one.
|
# the correct one.
|
||||||
action = "Replaced header in" if has_any_header else "Added header to"
|
action = "Replaced header in" if has_any_header else "Added header to"
|
||||||
new_content = HEADER_NORMALIZED + '\n\n' + body
|
new_content = HEADER_NORMALIZED + '\n\n' + body
|
||||||
|
|
@ -106,7 +106,7 @@ def process_directory(root_dir):
|
||||||
files_modified += 1
|
files_modified += 1
|
||||||
else:
|
else:
|
||||||
if has_any_header:
|
if has_any_header:
|
||||||
# Remove the header — it shouldn't be here.
|
# Remove the header - it shouldn't be here.
|
||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(body)
|
f.write(body)
|
||||||
print(f"Removed header from: {file_path}")
|
print(f"Removed header from: {file_path}")
|
||||||
|
|
@ -134,4 +134,4 @@ if __name__ == "__main__":
|
||||||
print(f"Error: Directory '{target_directory}' not found.")
|
print(f"Error: Directory '{target_directory}' not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
process_directory(os.path.abspath(target_directory))
|
process_directory(os.path.abspath(target_directory))
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
|
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
|
||||||
"introTitle": "Управлявано Самостоятелно-хостван Панголиин",
|
"introTitle": "Управлявано Самостоятелно-хостван Панголиин",
|
||||||
"introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.",
|
"introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.",
|
||||||
"introDetail": "С тази опция все още управлявате свой собствен Панголиин възел — вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:",
|
"introDetail": "С тази опция все още управлявате свой собствен Панголиин възел - вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "По-прости операции",
|
"title": "По-прости операции",
|
||||||
"description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало."
|
"description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало."
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Разкриване на употреба",
|
"title": "Разкриване на употреба",
|
||||||
"description": "Изберете лицензионен клас, който точно отразява вашата целена употреба. Персоналният лиценз позволява безплатно ползване на софтуера за индивидуална, некомерсиална или маломащабна комерсиална дейност с годишен брутен приход под 100,000 USD. Всяко ползване извън тези граници — включително ползване във фирма, организация или друга доходоносна среда — изисква валиден корпоративен лиценз и плащане на съответната лицензионна такса. Всички потребители, независимо дали са лични или корпоративни, трябва да спазват Условията на Fossorial Commercial License."
|
"description": "Изберете лицензионен клас, който точно отразява вашата целена употреба. Персоналният лиценз позволява безплатно ползване на софтуера за индивидуална, некомерсиална или маломащабна комерсиална дейност с годишен брутен приход под 100,000 USD. Всяко ползване извън тези граници - включително ползване във фирма, организация или друга доходоносна среда - изисква валиден корпоративен лиценз и плащане на съответната лицензионна такса. Всички потребители, независимо дали са лични или корпоративни, трябва да спазват Условията на Fossorial Commercial License."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Информация за пробен период",
|
"title": "Информация за пробен период",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON масив",
|
"httpDestFormatJsonArrayTitle": "JSON масив",
|
||||||
"httpDestFormatJsonArrayDescription": "Една заявка на партида, тялото е JSON масив. Съвместим с повечето общи уеб куки и Datadog.",
|
"httpDestFormatJsonArrayDescription": "Една заявка на партида, тялото е JSON масив. Съвместим с повечето общи уеб куки и Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Една заявка на партида, тялото е ново линии отделени JSON — един обект на ред, няма външен масив. Изисквано от Splunk HEC, Elastic / OpenSearch и Grafana.",
|
"httpDestFormatNdjsonDescription": "Една заявка на партида, тялото е ново линии отделени JSON - един обект на ред, няма външен масив. Изисквано от Splunk HEC, Elastic / OpenSearch и Grafana.",
|
||||||
"httpDestFormatSingleTitle": "Едно събитие на заявка",
|
"httpDestFormatSingleTitle": "Едно събитие на заявка",
|
||||||
"httpDestFormatSingleDescription": "Изпращат се отделни HTTP POST за всяко индивидуално събитие. Използвайте само за крайни точки, които не могат да обработват партиди.",
|
"httpDestFormatSingleDescription": "Изпращат се отделни HTTP POST за всяко индивидуално събитие. Използвайте само за крайни точки, които не могат да обработват партиди.",
|
||||||
"httpDestLogTypesTitle": "Видове логове",
|
"httpDestLogTypesTitle": "Видове логове",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami",
|
"description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami",
|
||||||
"introTitle": "Spravovaný Pangolin",
|
"introTitle": "Spravovaný Pangolin",
|
||||||
"introDescription": "je možnost nasazení určená pro lidi, kteří chtějí jednoduchost a spolehlivost při zachování soukromých a samoobslužných dat.",
|
"introDescription": "je možnost nasazení určená pro lidi, kteří chtějí jednoduchost a spolehlivost při zachování soukromých a samoobslužných dat.",
|
||||||
"introDetail": "Pomocí této volby stále provozujete vlastní uzel Pangolin — tunely, SSL terminály a provoz všech pobytů na vašem serveru. Rozdíl spočívá v tom, že řízení a monitorování se řeší prostřednictvím našeho cloudového panelu, který odemkne řadu výhod:",
|
"introDetail": "Pomocí této volby stále provozujete vlastní uzel Pangolin - tunely, SSL terminály a provoz všech pobytů na vašem serveru. Rozdíl spočívá v tom, že řízení a monitorování se řeší prostřednictvím našeho cloudového panelu, který odemkne řadu výhod:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Jednoduchý provoz",
|
"title": "Jednoduchý provoz",
|
||||||
"description": "Není třeba spouštět svůj vlastní poštovní server nebo nastavit komplexní upozornění. Ze schránky dostanete upozornění na zdravotní kontrolu a výpadek."
|
"description": "Není třeba spouštět svůj vlastní poštovní server nebo nastavit komplexní upozornění. Ze schránky dostanete upozornění na zdravotní kontrolu a výpadek."
|
||||||
|
|
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Verwendungsanzeige",
|
"title": "Verwendungsanzeige",
|
||||||
"description": "Wählen Sie die Lizenz-Ebene, die Ihre beabsichtigte Nutzung genau widerspiegelt. Die Persönliche Lizenz erlaubt die freie Nutzung der Software für individuelle, nicht-kommerzielle oder kleine kommerzielle Aktivitäten mit jährlichen Brutto-Einnahmen von 100.000 USD. Über diese Grenzen hinausgehende Verwendungszwecke – einschließlich der Verwendung innerhalb eines Unternehmens, einer Organisation, oder eine andere umsatzgenerierende Umgebung — erfordert eine gültige Enterprise-Lizenz und die Zahlung der Lizenzgebühr. Alle Benutzer, ob Personal oder Enterprise, müssen die Fossorial Commercial License Bedingungen einhalten."
|
"description": "Wählen Sie die Lizenz-Ebene, die Ihre beabsichtigte Nutzung genau widerspiegelt. Die Persönliche Lizenz erlaubt die freie Nutzung der Software für individuelle, nicht-kommerzielle oder kleine kommerzielle Aktivitäten mit jährlichen Brutto-Einnahmen von 100.000 USD. Über diese Grenzen hinausgehende Verwendungszwecke – einschließlich der Verwendung innerhalb eines Unternehmens, einer Organisation, oder eine andere umsatzgenerierende Umgebung - erfordert eine gültige Enterprise-Lizenz und die Zahlung der Lizenzgebühr. Alle Benutzer, ob Personal oder Enterprise, müssen die Fossorial Commercial License Bedingungen einhalten."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Testperiode Information",
|
"title": "Testperiode Information",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||||
"httpDestFormatJsonArrayDescription": "Eine Anfrage pro Stapel ist ein JSON-Array. Kompatibel mit den meisten generischen Webhooks und Datadog.",
|
"httpDestFormatJsonArrayDescription": "Eine Anfrage pro Stapel ist ein JSON-Array. Kompatibel mit den meisten generischen Webhooks und Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Eine Anfrage pro Batch, der Körper ist newline-getrenntes JSON — ein Objekt pro Zeile, kein äußeres Array. Benötigt von Splunk HEC, Elastic / OpenSearch, und Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Eine Anfrage pro Batch, der Körper ist newline-getrenntes JSON - ein Objekt pro Zeile, kein äußeres Array. Benötigt von Splunk HEC, Elastic / OpenSearch, und Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Ein Ereignis pro Anfrage",
|
"httpDestFormatSingleTitle": "Ein Ereignis pro Anfrage",
|
||||||
"httpDestFormatSingleDescription": "Sendet eine separate HTTP-POST für jedes einzelne Ereignis. Nur für Endpunkte, die Batches nicht handhaben können.",
|
"httpDestFormatSingleDescription": "Sendet eine separate HTTP-POST für jedes einzelne Ereignis. Nur für Endpunkte, die Batches nicht handhaben können.",
|
||||||
"httpDestLogTypesTitle": "Log-Typen",
|
"httpDestLogTypesTitle": "Log-Typen",
|
||||||
|
|
|
||||||
|
|
@ -1346,7 +1346,7 @@
|
||||||
"sidebarManagement": "Management",
|
"sidebarManagement": "Management",
|
||||||
"sidebarBillingAndLicenses": "Billing & Licenses",
|
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||||
"sidebarLogsAnalytics": "Analytics",
|
"sidebarLogsAnalytics": "Analytics",
|
||||||
"alertingTitle": "Alerting rules",
|
"alertingTitle": "Alerting",
|
||||||
"alertingDescription": "Define sources, triggers, and actions for notifications.",
|
"alertingDescription": "Define sources, triggers, and actions for notifications.",
|
||||||
"alertingRules": "Alert rules",
|
"alertingRules": "Alert rules",
|
||||||
"alertingSearchRules": "Search rules…",
|
"alertingSearchRules": "Search rules…",
|
||||||
|
|
@ -1399,7 +1399,7 @@
|
||||||
"alertingSelectHealthChecks": "Select health checks…",
|
"alertingSelectHealthChecks": "Select health checks…",
|
||||||
"alertingHealthChecksSelected": "{count} health checks selected",
|
"alertingHealthChecksSelected": "{count} health checks selected",
|
||||||
"alertingNoHealthChecks": "No targets with health checks enabled",
|
"alertingNoHealthChecks": "No targets with health checks enabled",
|
||||||
"alertingHealthCheckStub": "Health check source selection is not wired up yet — you can still configure triggers and actions.",
|
"alertingHealthCheckStub": "Health check source selection is not wired up yet - you can still configure triggers and actions.",
|
||||||
"alertingSelectUsers": "Select users…",
|
"alertingSelectUsers": "Select users…",
|
||||||
"alertingUsersSelected": "{count} users selected",
|
"alertingUsersSelected": "{count} users selected",
|
||||||
"alertingSelectRoles": "Select roles…",
|
"alertingSelectRoles": "Select roles…",
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"alertingConfigureTrigger": "Configure Trigger",
|
"alertingConfigureTrigger": "Configure Trigger",
|
||||||
"alertingConfigureActions": "Configure Actions",
|
"alertingConfigureActions": "Configure Actions",
|
||||||
"alertingBackToRules": "Back to Rules",
|
"alertingBackToRules": "Back to Rules",
|
||||||
"alertingDraftBadge": "Draft — save to store this rule",
|
"alertingDraftBadge": "Draft - save to store this rule",
|
||||||
"alertingSidebarHint": "Click a step on the canvas to edit it here.",
|
"alertingSidebarHint": "Click a step on the canvas to edit it here.",
|
||||||
"alertingGraphCanvasTitle": "Rule Flow",
|
"alertingGraphCanvasTitle": "Rule Flow",
|
||||||
"alertingGraphCanvasDescription": "Visual overview of source, trigger, and actions. Select a node to edit it in the panel.",
|
"alertingGraphCanvasDescription": "Visual overview of source, trigger, and actions. Select a node to edit it in the panel.",
|
||||||
|
|
@ -2115,7 +2115,7 @@
|
||||||
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||||
"introTitle": "Managed Self-Hosted Pangolin",
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
||||||
"introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
"introDetail": "With this option, you still run your own Pangolin node - your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Simpler operations",
|
"title": "Simpler operations",
|
||||||
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
||||||
|
|
@ -2240,7 +2240,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||||
"domainPickerProvidedDomain": "Provided Domain",
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
"domainPickerFreeProvidedDomain": "Provided Domain",
|
"domainPickerFreeProvidedDomain": "Provided Domain",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan — no need to bring your own.",
|
"domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan - no need to bring your own.",
|
||||||
"domainPickerVerified": "Verified",
|
"domainPickerVerified": "Verified",
|
||||||
"domainPickerUnverified": "Unverified",
|
"domainPickerUnverified": "Unverified",
|
||||||
"domainPickerManual": "Manual",
|
"domainPickerManual": "Manual",
|
||||||
|
|
@ -2418,7 +2418,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Usage Disclosure",
|
"title": "Usage Disclosure",
|
||||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits - including use within a business, organization, or other revenue-generating environment - requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Trial Period Information",
|
"title": "Trial Period Information",
|
||||||
|
|
@ -3010,7 +3010,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||||
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
|
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON - one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "One Event Per Request",
|
"httpDestFormatSingleTitle": "One Event Per Request",
|
||||||
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
|
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
|
||||||
"httpDestLogTypesTitle": "Log Types",
|
"httpDestLogTypesTitle": "Log Types",
|
||||||
|
|
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización",
|
"selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización",
|
||||||
"domainPickerProvidedDomain": "Dominio proporcionado",
|
"domainPickerProvidedDomain": "Dominio proporcionado",
|
||||||
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Los dominios proporcionados son una función de pago. Suscríbete para obtener un dominio incluido con tu plan — no necesitas traer el tuyo propio.",
|
"domainPickerFreeDomainsPaidFeature": "Los dominios proporcionados son una función de pago. Suscríbete para obtener un dominio incluido con tu plan - no necesitas traer el tuyo propio.",
|
||||||
"domainPickerVerified": "Verificado",
|
"domainPickerVerified": "Verificado",
|
||||||
"domainPickerUnverified": "Sin verificar",
|
"domainPickerUnverified": "Sin verificar",
|
||||||
"domainPickerManual": "Manual",
|
"domainPickerManual": "Manual",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Divulgación de uso",
|
"title": "Divulgación de uso",
|
||||||
"description": "Seleccione el nivel de licencia que refleje con precisión su uso previsto. La Licencia Personal permite el uso libre del Software para actividades comerciales individuales, no comerciales o de pequeña escala con ingresos brutos anuales inferiores a $100,000 USD. Cualquier uso más allá de estos límites — incluyendo el uso dentro de una empresa, organización, u otro entorno de generación de ingresos — requiere una Licencia Empresarial válida y el pago de la cuota de licencia aplicable. Todos los usuarios, ya sean personales o empresariales, deben cumplir con las Condiciones de Licencia Comercial Fossorial."
|
"description": "Seleccione el nivel de licencia que refleje con precisión su uso previsto. La Licencia Personal permite el uso libre del Software para actividades comerciales individuales, no comerciales o de pequeña escala con ingresos brutos anuales inferiores a $100,000 USD. Cualquier uso más allá de estos límites - incluyendo el uso dentro de una empresa, organización, u otro entorno de generación de ingresos - requiere una Licencia Empresarial válida y el pago de la cuota de licencia aplicable. Todos los usuarios, ya sean personales o empresariales, deben cumplir con las Condiciones de Licencia Comercial Fossorial."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Información del período de prueba",
|
"title": "Información del período de prueba",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
||||||
"httpDestFormatJsonArrayDescription": "Una petición por lote, cuerpo es una matriz JSON. Compatible con la mayoría de los webhooks y Datadog.",
|
"httpDestFormatJsonArrayDescription": "Una petición por lote, cuerpo es una matriz JSON. Compatible con la mayoría de los webhooks y Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Una petición por lote, el cuerpo es JSON delimitado por línea — un objeto por línea, sin arrays externos. Requerido por Splunk HEC, Elastic / OpenSearch, y Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Una petición por lote, el cuerpo es JSON delimitado por línea - un objeto por línea, sin arrays externos. Requerido por Splunk HEC, Elastic / OpenSearch, y Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Un evento por solicitud",
|
"httpDestFormatSingleTitle": "Un evento por solicitud",
|
||||||
"httpDestFormatSingleDescription": "Envía un HTTP POST separado para cada evento individual. Úsalo sólo para los extremos que no pueden manejar lotes.",
|
"httpDestFormatSingleDescription": "Envía un HTTP POST separado para cada evento individual. Úsalo sólo para los extremos que no pueden manejar lotes.",
|
||||||
"httpDestLogTypesTitle": "Tipos de Log",
|
"httpDestLogTypesTitle": "Tipos de Log",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
|
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
|
||||||
"introTitle": "Pangolin auto-hébergé géré",
|
"introTitle": "Pangolin auto-hébergé géré",
|
||||||
"introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.",
|
"introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.",
|
||||||
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin — vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
|
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin - vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Opérations plus simples",
|
"title": "Opérations plus simples",
|
||||||
"description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite."
|
"description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite."
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation",
|
"selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation",
|
||||||
"domainPickerProvidedDomain": "Domaine fourni",
|
"domainPickerProvidedDomain": "Domaine fourni",
|
||||||
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Les domaines fournis sont une fonctionnalité payante. Abonnez-vous pour obtenir un domaine inclus avec votre plan — plus besoin de fournir le vôtre.",
|
"domainPickerFreeDomainsPaidFeature": "Les domaines fournis sont une fonctionnalité payante. Abonnez-vous pour obtenir un domaine inclus avec votre plan - plus besoin de fournir le vôtre.",
|
||||||
"domainPickerVerified": "Vérifié",
|
"domainPickerVerified": "Vérifié",
|
||||||
"domainPickerUnverified": "Non vérifié",
|
"domainPickerUnverified": "Non vérifié",
|
||||||
"domainPickerManual": "Manuel",
|
"domainPickerManual": "Manuel",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Divulgation d'utilisation",
|
"title": "Divulgation d'utilisation",
|
||||||
"description": "Sélectionnez le niveau de licence qui correspond exactement à votre utilisation prévue. La Licence Personnelle autorise l'utilisation libre du Logiciel pour des activités commerciales individuelles, non commerciales ou à petite échelle avec un revenu annuel brut inférieur à 100 000 USD. Toute utilisation au-delà de ces limites — y compris l'utilisation au sein d'une entreprise, d'une organisation, ou tout autre environnement générateur de revenus — nécessite une licence d’entreprise valide et le paiement des droits de licence applicables. Tous les utilisateurs, qu'ils soient personnels ou d'entreprise, doivent se conformer aux conditions de licence commerciale Fossorial."
|
"description": "Sélectionnez le niveau de licence qui correspond exactement à votre utilisation prévue. La Licence Personnelle autorise l'utilisation libre du Logiciel pour des activités commerciales individuelles, non commerciales ou à petite échelle avec un revenu annuel brut inférieur à 100 000 USD. Toute utilisation au-delà de ces limites - y compris l'utilisation au sein d'une entreprise, d'une organisation, ou tout autre environnement générateur de revenus - nécessite une licence d’entreprise valide et le paiement des droits de licence applicables. Tous les utilisateurs, qu'ils soient personnels ou d'entreprise, doivent se conformer aux conditions de licence commerciale Fossorial."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Informations sur la période d'essai",
|
"title": "Informations sur la période d'essai",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "Tableau JSON",
|
"httpDestFormatJsonArrayTitle": "Tableau JSON",
|
||||||
"httpDestFormatJsonArrayDescription": "Une requête par lot, le corps est un tableau JSON. Compatible avec la plupart des webhooks génériques et des datadog.",
|
"httpDestFormatJsonArrayDescription": "Une requête par lot, le corps est un tableau JSON. Compatible avec la plupart des webhooks génériques et des datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Une requête par lot, body est un JSON délimité par une nouvelle ligne — un objet par ligne, pas de tableau extérieur. Requis par Splunk HEC, Elastic / OpenSearch, et Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Une requête par lot, body est un JSON délimité par une nouvelle ligne - un objet par ligne, pas de tableau extérieur. Requis par Splunk HEC, Elastic / OpenSearch, et Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Un événement par demande",
|
"httpDestFormatSingleTitle": "Un événement par demande",
|
||||||
"httpDestFormatSingleDescription": "Envoie un POST HTTP séparé pour chaque événement individuel. Utilisé uniquement pour les terminaux qui ne peuvent pas gérer des lots.",
|
"httpDestFormatSingleDescription": "Envoie un POST HTTP séparé pour chaque événement individuel. Utilisé uniquement pour les terminaux qui ne peuvent pas gérer des lots.",
|
||||||
"httpDestLogTypesTitle": "Types de logs",
|
"httpDestLogTypesTitle": "Types de logs",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
|
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
|
||||||
"introTitle": "Managed Self-Hosted Pangolin",
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
"introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.",
|
"introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.",
|
||||||
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin — i tunnel, la terminazione SSL e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:",
|
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin - i tunnel, la terminazione SSL e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Operazioni più semplici",
|
"title": "Operazioni più semplici",
|
||||||
"description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella."
|
"description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella."
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione",
|
"selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione",
|
||||||
"domainPickerProvidedDomain": "Dominio Fornito",
|
"domainPickerProvidedDomain": "Dominio Fornito",
|
||||||
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||||
"domainPickerFreeDomainsPaidFeature": "I domini forniti sono una funzionalità a pagamento. Abbonati per ricevere un dominio incluso con il tuo piano — non è necessario portare il proprio.",
|
"domainPickerFreeDomainsPaidFeature": "I domini forniti sono una funzionalità a pagamento. Abbonati per ricevere un dominio incluso con il tuo piano - non è necessario portare il proprio.",
|
||||||
"domainPickerVerified": "Verificato",
|
"domainPickerVerified": "Verificato",
|
||||||
"domainPickerUnverified": "Non Verificato",
|
"domainPickerUnverified": "Non Verificato",
|
||||||
"domainPickerManual": "Manuale",
|
"domainPickerManual": "Manuale",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Trasparenza Di Utilizzo",
|
"title": "Trasparenza Di Utilizzo",
|
||||||
"description": "Seleziona il livello di licenza che rispecchia accuratamente il tuo utilizzo previsto. La Licenza Personale consente l'uso gratuito del Software per le attività commerciali individuali, non commerciali o su piccola scala con entrate lorde annue inferiori a $100.000 USD. Qualsiasi uso oltre questi limiti — compreso l'uso all'interno di un'azienda, organizzazione, o altro ambiente generatore di entrate — richiede una licenza Enterprise valida e il pagamento della tassa di licenza applicabile. Tutti gli utenti, siano essi personali o aziendali, devono rispettare i termini di licenza commerciale Fossorial."
|
"description": "Seleziona il livello di licenza che rispecchia accuratamente il tuo utilizzo previsto. La Licenza Personale consente l'uso gratuito del Software per le attività commerciali individuali, non commerciali o su piccola scala con entrate lorde annue inferiori a $100.000 USD. Qualsiasi uso oltre questi limiti - compreso l'uso all'interno di un'azienda, organizzazione, o altro ambiente generatore di entrate - richiede una licenza Enterprise valida e il pagamento della tassa di licenza applicabile. Tutti gli utenti, siano essi personali o aziendali, devono rispettare i termini di licenza commerciale Fossorial."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Informazioni Periodo Di Prova",
|
"title": "Informazioni Periodo Di Prova",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||||
"httpDestFormatJsonArrayDescription": "Una richiesta per lotto, corpo è un array JSON. Compatibile con la maggior parte dei webhooks generici e Datadog.",
|
"httpDestFormatJsonArrayDescription": "Una richiesta per lotto, corpo è un array JSON. Compatibile con la maggior parte dei webhooks generici e Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Una richiesta per lotto, corpo è newline-delimited JSON — un oggetto per linea, nessun array esterno. Richiesto da Splunk HEC, Elastic / OpenSearch, e Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Una richiesta per lotto, corpo è newline-delimited JSON - un oggetto per linea, nessun array esterno. Richiesto da Splunk HEC, Elastic / OpenSearch, e Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Un Evento Per Richiesta",
|
"httpDestFormatSingleTitle": "Un Evento Per Richiesta",
|
||||||
"httpDestFormatSingleDescription": "Invia un HTTP POST separato per ogni singolo evento. Usa solo per gli endpoint che non possono gestire i batch.",
|
"httpDestFormatSingleDescription": "Invia un HTTP POST separato per ogni singolo evento. Usa solo per gli endpoint che non possono gestire i batch.",
|
||||||
"httpDestLogTypesTitle": "Tipi Di Log",
|
"httpDestLogTypesTitle": "Tipi Di Log",
|
||||||
|
|
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.",
|
"selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.",
|
||||||
"domainPickerProvidedDomain": "제공된 도메인",
|
"domainPickerProvidedDomain": "제공된 도메인",
|
||||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||||
"domainPickerFreeDomainsPaidFeature": "제공된 도메인은 유료 기능입니다. 요금제에 도메인이 포함되도록 구독하세요. — 별도로 도메인을 준비할 필요 없습니다.",
|
"domainPickerFreeDomainsPaidFeature": "제공된 도메인은 유료 기능입니다. 요금제에 도메인이 포함되도록 구독하세요. - 별도로 도메인을 준비할 필요 없습니다.",
|
||||||
"domainPickerVerified": "검증됨",
|
"domainPickerVerified": "검증됨",
|
||||||
"domainPickerUnverified": "검증되지 않음",
|
"domainPickerUnverified": "검증되지 않음",
|
||||||
"domainPickerManual": "수동",
|
"domainPickerManual": "수동",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "사용 공개",
|
"title": "사용 공개",
|
||||||
"description": "당신의 의도된 사용에 정확히 맞는 라이선스 등급을 선택하세요. 개인 라이선스는 연간 총 수익 100,000 USD 이하의 개인, 비상업적 또는 소규모 상업 활동을 위한 소프트웨어의 무료 사용을 허용합니다. 이러한 제한을 넘는 모든 사용 — 비즈니스, 조직 또는 기타 수익 창출 환경 내에서의 사용 — 은 유효한 엔터프라이즈 라이선스 및 해당 라이선스 수수료의 지불이 필요합니다. 개인 또는 기업 사용자는 모두 Fossorial 상용 라이선스 조건을 준수해야 합니다."
|
"description": "당신의 의도된 사용에 정확히 맞는 라이선스 등급을 선택하세요. 개인 라이선스는 연간 총 수익 100,000 USD 이하의 개인, 비상업적 또는 소규모 상업 활동을 위한 소프트웨어의 무료 사용을 허용합니다. 이러한 제한을 넘는 모든 사용 - 비즈니스, 조직 또는 기타 수익 창출 환경 내에서의 사용 - 은 유효한 엔터프라이즈 라이선스 및 해당 라이선스 수수료의 지불이 필요합니다. 개인 또는 기업 사용자는 모두 Fossorial 상용 라이선스 조건을 준수해야 합니다."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "시험 기간 정보",
|
"title": "시험 기간 정보",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON 배열",
|
"httpDestFormatJsonArrayTitle": "JSON 배열",
|
||||||
"httpDestFormatJsonArrayDescription": "각 배치마다 요청 하나씩, 본문은 JSON 배열입니다. 대부분의 일반 웹훅 및 Datadog과 호환됩니다.",
|
"httpDestFormatJsonArrayDescription": "각 배치마다 요청 하나씩, 본문은 JSON 배열입니다. 대부분의 일반 웹훅 및 Datadog과 호환됩니다.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "각 배치마다 요청 하나씩, 본문은 줄 구분 JSON — 한 라인에 하나의 객체가 있으며 외부 배열이 없습니다. Splunk HEC, Elastic / OpenSearch, Grafana Loki에 필요합니다.",
|
"httpDestFormatNdjsonDescription": "각 배치마다 요청 하나씩, 본문은 줄 구분 JSON - 한 라인에 하나의 객체가 있으며 외부 배열이 없습니다. Splunk HEC, Elastic / OpenSearch, Grafana Loki에 필요합니다.",
|
||||||
"httpDestFormatSingleTitle": "각 요청 당 하나의 이벤트",
|
"httpDestFormatSingleTitle": "각 요청 당 하나의 이벤트",
|
||||||
"httpDestFormatSingleDescription": "각 개별 이벤트에 대해 별도의 HTTP POST를 전송합니다. 배치를 처리할 수 없는 엔드포인트에만 사용하세요.",
|
"httpDestFormatSingleDescription": "각 개별 이벤트에 대해 별도의 HTTP POST를 전송합니다. 배치를 처리할 수 없는 엔드포인트에만 사용하세요.",
|
||||||
"httpDestLogTypesTitle": "로그 유형",
|
"httpDestLogTypesTitle": "로그 유형",
|
||||||
|
|
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON liste",
|
"httpDestFormatJsonArrayTitle": "JSON liste",
|
||||||
"httpDestFormatJsonArrayDescription": "Én forespørsel per batch, innholdet er en JSON-liste. Kompatibel med de mest generiske webhooks og Datadog.",
|
"httpDestFormatJsonArrayDescription": "Én forespørsel per batch, innholdet er en JSON-liste. Kompatibel med de mest generiske webhooks og Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON — et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON - et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "En hendelse per forespørsel",
|
"httpDestFormatSingleTitle": "En hendelse per forespørsel",
|
||||||
"httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hendelse. Bruk bare for endepunkter som ikke kan håndtere batcher.",
|
"httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hendelse. Bruk bare for endepunkter som ikke kan håndtere batcher.",
|
||||||
"httpDestLogTypesTitle": "Logg typer",
|
"httpDestLogTypesTitle": "Logg typer",
|
||||||
|
|
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie",
|
"selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie",
|
||||||
"domainPickerProvidedDomain": "Opgegeven domein",
|
"domainPickerProvidedDomain": "Opgegeven domein",
|
||||||
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen — je hoeft er zelf geen mee te brengen.",
|
"domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen - je hoeft er zelf geen mee te brengen.",
|
||||||
"domainPickerVerified": "Geverifieerd",
|
"domainPickerVerified": "Geverifieerd",
|
||||||
"domainPickerUnverified": "Ongeverifieerd",
|
"domainPickerUnverified": "Ongeverifieerd",
|
||||||
"domainPickerManual": "Handleiding",
|
"domainPickerManual": "Handleiding",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
|
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
|
||||||
"introTitle": "Zarządzany samowystarczalny Pangolin",
|
"introTitle": "Zarządzany samowystarczalny Pangolin",
|
||||||
"introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.",
|
"introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.",
|
||||||
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin — tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
|
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin - tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Uproszczone operacje",
|
"title": "Uproszczone operacje",
|
||||||
"description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju."
|
"description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju."
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji",
|
"selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji",
|
||||||
"domainPickerProvidedDomain": "Dostarczona domena",
|
"domainPickerProvidedDomain": "Dostarczona domena",
|
||||||
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Dostarczane domeny to funkcja płatna. Subskrybuj, aby uzyskać domenę w ramach swojego planu — nie ma potrzeby przynoszenia własnej.",
|
"domainPickerFreeDomainsPaidFeature": "Dostarczane domeny to funkcja płatna. Subskrybuj, aby uzyskać domenę w ramach swojego planu - nie ma potrzeby przynoszenia własnej.",
|
||||||
"domainPickerVerified": "Zweryfikowano",
|
"domainPickerVerified": "Zweryfikowano",
|
||||||
"domainPickerUnverified": "Niezweryfikowane",
|
"domainPickerUnverified": "Niezweryfikowane",
|
||||||
"domainPickerManual": "Podręcznik",
|
"domainPickerManual": "Podręcznik",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "Tablica JSON",
|
"httpDestFormatJsonArrayTitle": "Tablica JSON",
|
||||||
"httpDestFormatJsonArrayDescription": "Jedna prośba na partię, treść jest tablicą JSON. Kompatybilna z najbardziej ogólnymi webhookami i Datadog.",
|
"httpDestFormatJsonArrayDescription": "Jedna prośba na partię, treść jest tablicą JSON. Kompatybilna z najbardziej ogólnymi webhookami i Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Jedno żądanie na partię, ciałem jest plik JSON rozdzielony na newline-delimited — jeden obiekt na wiersz, bez tablicy zewnętrznej. Wymagane przez Splunk HEC, Elastic / OpenSesearch i Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Jedno żądanie na partię, ciałem jest plik JSON rozdzielony na newline-delimited - jeden obiekt na wiersz, bez tablicy zewnętrznej. Wymagane przez Splunk HEC, Elastic / OpenSesearch i Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Jedno wydarzenie na żądanie",
|
"httpDestFormatSingleTitle": "Jedno wydarzenie na żądanie",
|
||||||
"httpDestFormatSingleDescription": "Wysyła oddzielny POST HTTP dla każdego zdarzenia. Użyj tylko dla punktów końcowych, które nie mogą obsługiwać partii.",
|
"httpDestFormatSingleDescription": "Wysyła oddzielny POST HTTP dla każdego zdarzenia. Użyj tylko dla punktów końcowych, które nie mogą obsługiwać partii.",
|
||||||
"httpDestLogTypesTitle": "Typy logów",
|
"httpDestLogTypesTitle": "Typy logów",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
|
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
|
||||||
"introTitle": "Pangolin Auto-Hospedado Gerenciado",
|
"introTitle": "Pangolin Auto-Hospedado Gerenciado",
|
||||||
"introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.",
|
"introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.",
|
||||||
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin — seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
|
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin - seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Operações simples",
|
"title": "Operações simples",
|
||||||
"description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade."
|
"description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade."
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Selecione um domínio para a página de autenticação da organização",
|
"selectDomainForOrgAuthPage": "Selecione um domínio para a página de autenticação da organização",
|
||||||
"domainPickerProvidedDomain": "Domínio fornecido",
|
"domainPickerProvidedDomain": "Domínio fornecido",
|
||||||
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Os domínios fornecidos são um recurso pago. Assine para obter um domínio incluído no seu plano — não há necessidade de trazer o seu próprio.",
|
"domainPickerFreeDomainsPaidFeature": "Os domínios fornecidos são um recurso pago. Assine para obter um domínio incluído no seu plano - não há necessidade de trazer o seu próprio.",
|
||||||
"domainPickerVerified": "Verificada",
|
"domainPickerVerified": "Verificada",
|
||||||
"domainPickerUnverified": "Não verificado",
|
"domainPickerUnverified": "Não verificado",
|
||||||
"domainPickerManual": "Manual",
|
"domainPickerManual": "Manual",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Divulgação de uso",
|
"title": "Divulgação de uso",
|
||||||
"description": "Selecione o nível de licença que reflete corretamente seu uso pretendido. A Licença Pessoal permite o uso livre do Software para atividades comerciais individuais, não comerciais ou em pequena escala com rendimento bruto anual inferior a 100.000 USD. Qualquer uso além destes limites — incluindo uso dentro de um negócio, organização, ou outro ambiente gerador de receitas — requer uma Licença Enterprise válida e o pagamento da taxa aplicável de licenciamento. Todos os usuários, pessoais ou empresariais, devem cumprir os Termos da Licença Comercial Fossorial."
|
"description": "Selecione o nível de licença que reflete corretamente seu uso pretendido. A Licença Pessoal permite o uso livre do Software para atividades comerciais individuais, não comerciais ou em pequena escala com rendimento bruto anual inferior a 100.000 USD. Qualquer uso além destes limites - incluindo uso dentro de um negócio, organização, ou outro ambiente gerador de receitas - requer uma Licença Enterprise válida e o pagamento da taxa aplicável de licenciamento. Todos os usuários, pessoais ou empresariais, devem cumprir os Termos da Licença Comercial Fossorial."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Informações do Período de Avaliação",
|
"title": "Informações do Período de Avaliação",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
||||||
"httpDestFormatJsonArrayDescription": "Um pedido por lote, o corpo é um array JSON. Compatível com a maioria dos webhooks genéricos e Datadog.",
|
"httpDestFormatJsonArrayDescription": "Um pedido por lote, o corpo é um array JSON. Compatível com a maioria dos webhooks genéricos e Datadog.",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "Um pedido por lote, o corpo é um JSON delimitado por nova-linha — um objeto por linha, sem array exterior. Requerido pelo Splunk HEC, Elástico / OpenSearch, e Grafana Loki.",
|
"httpDestFormatNdjsonDescription": "Um pedido por lote, o corpo é um JSON delimitado por nova-linha - um objeto por linha, sem array exterior. Requerido pelo Splunk HEC, Elástico / OpenSearch, e Grafana Loki.",
|
||||||
"httpDestFormatSingleTitle": "Um Evento por Requisição",
|
"httpDestFormatSingleTitle": "Um Evento por Requisição",
|
||||||
"httpDestFormatSingleDescription": "Envia um POST HTTP separado para cada evento. Utilize apenas para endpoints que não podem manipular lotes.",
|
"httpDestFormatSingleDescription": "Envia um POST HTTP separado para cada evento. Utilize apenas para endpoints que não podem manipular lotes.",
|
||||||
"httpDestLogTypesTitle": "Tipos de log",
|
"httpDestLogTypesTitle": "Tipos de log",
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
"siteManageSites": "Управление сайтами",
|
"siteManageSites": "Управление сайтами",
|
||||||
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
|
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
|
||||||
"sitesBannerTitle": "Подключить любую сеть",
|
"sitesBannerTitle": "Подключить любую сеть",
|
||||||
"sitesBannerDescription": "Сайт — это соединение с удаленной сетью, которое позволяет Pangolin предоставлять доступ к ресурсам, будь они общедоступными или частными, пользователям в любом месте. Установите сетевой коннектор сайта (Newt) там, где можно запустить исполняемый файл или контейнер, чтобы установить соединение.",
|
"sitesBannerDescription": "Сайт - это соединение с удаленной сетью, которое позволяет Pangolin предоставлять доступ к ресурсам, будь они общедоступными или частными, пользователям в любом месте. Установите сетевой коннектор сайта (Newt) там, где можно запустить исполняемый файл или контейнер, чтобы установить соединение.",
|
||||||
"sitesBannerButtonText": "Установить сайт",
|
"sitesBannerButtonText": "Установить сайт",
|
||||||
"approvalsBannerTitle": "Одобрить или запретить доступ к устройству",
|
"approvalsBannerTitle": "Одобрить или запретить доступ к устройству",
|
||||||
"approvalsBannerDescription": "Просмотрите и подтвердите или отклоните запросы на доступ к устройству от пользователей. Когда требуется подтверждение устройства, пользователи должны получить одобрение администратора, прежде чем их устройства смогут подключиться к ресурсам вашей организации.",
|
"approvalsBannerDescription": "Просмотрите и подтвердите или отклоните запросы на доступ к устройству от пользователей. Когда требуется подтверждение устройства, пользователи должны получить одобрение администратора, прежде чем их устройства смогут подключиться к ресурсам вашей организации.",
|
||||||
|
|
@ -163,7 +163,7 @@
|
||||||
"proxyResourceTitle": "Управление публичными ресурсами",
|
"proxyResourceTitle": "Управление публичными ресурсами",
|
||||||
"proxyResourceDescription": "Создание и управление ресурсами, которые доступны через веб-браузер",
|
"proxyResourceDescription": "Создание и управление ресурсами, которые доступны через веб-браузер",
|
||||||
"proxyResourcesBannerTitle": "Общедоступный доступ через веб",
|
"proxyResourcesBannerTitle": "Общедоступный доступ через веб",
|
||||||
"proxyResourcesBannerDescription": "Общедоступные ресурсы — это прокси-по HTTPS или TCP/UDP, доступные любому пользователю в Интернете через веб-браузер. В отличие от частных ресурсов, они не требуют программного обеспечения на стороне клиента и могут включать политики доступа на основе идентификации и контекста.",
|
"proxyResourcesBannerDescription": "Общедоступные ресурсы - это прокси-по HTTPS или TCP/UDP, доступные любому пользователю в Интернете через веб-браузер. В отличие от частных ресурсов, они не требуют программного обеспечения на стороне клиента и могут включать политики доступа на основе идентификации и контекста.",
|
||||||
"clientResourceTitle": "Управление приватными ресурсами",
|
"clientResourceTitle": "Управление приватными ресурсами",
|
||||||
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
|
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
|
||||||
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
|
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
|
||||||
|
|
@ -371,7 +371,7 @@
|
||||||
"provisioningKeysUpdated": "Ключ подготовки обновлен",
|
"provisioningKeysUpdated": "Ключ подготовки обновлен",
|
||||||
"provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.",
|
"provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.",
|
||||||
"provisioningKeysBannerTitle": "Ключи подготовки сайта",
|
"provisioningKeysBannerTitle": "Ключи подготовки сайта",
|
||||||
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске - нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
||||||
"provisioningKeysBannerButtonText": "Узнать больше",
|
"provisioningKeysBannerButtonText": "Узнать больше",
|
||||||
"pendingSitesBannerTitle": "Ожидающие сайты",
|
"pendingSitesBannerTitle": "Ожидающие сайты",
|
||||||
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
|
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
|
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
|
||||||
"introTitle": "Управляемый Само-Хост Панголина",
|
"introTitle": "Управляемый Само-Хост Панголина",
|
||||||
"introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.",
|
"introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.",
|
||||||
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin — туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
|
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin - туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Более простые операции",
|
"title": "Более простые операции",
|
||||||
"description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки."
|
"description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки."
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации",
|
"selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации",
|
||||||
"domainPickerProvidedDomain": "Домен предоставлен",
|
"domainPickerProvidedDomain": "Домен предоставлен",
|
||||||
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||||
"domainPickerFreeDomainsPaidFeature": "Предоставленные домены являются платной функцией. Подпишитесь, чтобы получить домен, включенный в ваш план — не нужно приносить свой собственный.",
|
"domainPickerFreeDomainsPaidFeature": "Предоставленные домены являются платной функцией. Подпишитесь, чтобы получить домен, включенный в ваш план - не нужно приносить свой собственный.",
|
||||||
"domainPickerVerified": "Подтверждено",
|
"domainPickerVerified": "Подтверждено",
|
||||||
"domainPickerUnverified": "Не подтверждено",
|
"domainPickerUnverified": "Не подтверждено",
|
||||||
"domainPickerManual": "Ручной",
|
"domainPickerManual": "Ручной",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Раскрытие",
|
"title": "Раскрытие",
|
||||||
"description": "Выберите уровень лицензии, который точно отражает ваше предполагаемое использование. Личная Лицензия разрешает свободное использование Программного Обеспечения для частной, некоммерческой или малой коммерческой деятельности с годовым валовым доходом до $100 000 USD. Любое использование сверх этих пределов — включая использование в бизнесе, организацию, или другой приносящей доход среде — требует действительной лицензии предприятия и уплаты соответствующей лицензионной платы. Все пользователи, будь то Личные или Предприятия, обязаны соблюдать условия коммерческой лицензии Fossoral."
|
"description": "Выберите уровень лицензии, который точно отражает ваше предполагаемое использование. Личная Лицензия разрешает свободное использование Программного Обеспечения для частной, некоммерческой или малой коммерческой деятельности с годовым валовым доходом до $100 000 USD. Любое использование сверх этих пределов - включая использование в бизнесе, организацию, или другой приносящей доход среде - требует действительной лицензии предприятия и уплаты соответствующей лицензионной платы. Все пользователи, будь то Личные или Предприятия, обязаны соблюдать условия коммерческой лицензии Fossoral."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Информация о пробном периоде",
|
"title": "Информация о пробном периоде",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
|
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
|
||||||
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
|
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
|
||||||
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
|
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
|
||||||
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz — tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz - tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "Daha basit işlemler",
|
"title": "Daha basit işlemler",
|
||||||
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
|
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "Kullanım Açıklaması",
|
"title": "Kullanım Açıklaması",
|
||||||
"description": "Kullanım amacınızı doğru bir şekilde yansıtan lisans seviyesini seçin. Kişisel Lisans, yazılımın bireysel, ticari olmayan veya yıllık geliri 100,000 ABD Dolarının altında olan küçük ölçekli ticari faaliyetlerde ücretsiz kullanılmasına izin verir. Bu sınırların ötesinde kullanım — bir işletme, organizasyon veya diğer gelir getirici ortamlarda kullanım dahil olmak üzere — geçerli bir Kurumsal Lisans ve ilgili lisans ücretinin ödenmesini gerektirir. Tüm kullanıcılar, ister Kişisel ister Kurumsal, Fossorial Ticari Lisans Şartlarına uymalıdır."
|
"description": "Kullanım amacınızı doğru bir şekilde yansıtan lisans seviyesini seçin. Kişisel Lisans, yazılımın bireysel, ticari olmayan veya yıllık geliri 100,000 ABD Dolarının altında olan küçük ölçekli ticari faaliyetlerde ücretsiz kullanılmasına izin verir. Bu sınırların ötesinde kullanım - bir işletme, organizasyon veya diğer gelir getirici ortamlarda kullanım dahil olmak üzere - geçerli bir Kurumsal Lisans ve ilgili lisans ücretinin ödenmesini gerektirir. Tüm kullanıcılar, ister Kişisel ister Kurumsal, Fossorial Ticari Lisans Şartlarına uymalıdır."
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "Deneme Süresi Bilgileri",
|
"title": "Deneme Süresi Bilgileri",
|
||||||
|
|
|
||||||
|
|
@ -1993,7 +1993,7 @@
|
||||||
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
|
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
|
||||||
"introTitle": "托管自托管的潘戈林公司",
|
"introTitle": "托管自托管的潘戈林公司",
|
||||||
"introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。",
|
"introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。",
|
||||||
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 — — 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
|
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 - - 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "简单的操作",
|
"title": "简单的操作",
|
||||||
"description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。"
|
"description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。"
|
||||||
|
|
@ -2118,7 +2118,7 @@
|
||||||
"selectDomainForOrgAuthPage": "选择组织认证页面的域",
|
"selectDomainForOrgAuthPage": "选择组织认证页面的域",
|
||||||
"domainPickerProvidedDomain": "提供的域",
|
"domainPickerProvidedDomain": "提供的域",
|
||||||
"domainPickerFreeProvidedDomain": "免费提供的域",
|
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||||
"domainPickerFreeDomainsPaidFeature": "提供的域名是付费功能。订阅即可将域名包含在您的计划中—无需自带域名。",
|
"domainPickerFreeDomainsPaidFeature": "提供的域名是付费功能。订阅即可将域名包含在您的计划中-无需自带域名。",
|
||||||
"domainPickerVerified": "已验证",
|
"domainPickerVerified": "已验证",
|
||||||
"domainPickerUnverified": "未验证",
|
"domainPickerUnverified": "未验证",
|
||||||
"domainPickerManual": "手动",
|
"domainPickerManual": "手动",
|
||||||
|
|
@ -2296,7 +2296,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "使用情况披露",
|
"title": "使用情况披露",
|
||||||
"description": "选择能准确反映您预定用途的许可等级。 个人许可证允许对个人、非商业性或小型商业活动免费使用软件,年收入毛额不到100 000美元。 超出这些限度的任何用途,包括在企业、组织内的用途。 或其他创收环境——需要有效的企业许可证和支付适用的许可证费用。 所有用户,不论是个人还是企业,都必须遵守寄养商业许可证条款。"
|
"description": "选择能准确反映您预定用途的许可等级。 个人许可证允许对个人、非商业性或小型商业活动免费使用软件,年收入毛额不到100 000美元。 超出这些限度的任何用途,包括在企业、组织内的用途。 或其他创收环境--需要有效的企业许可证和支付适用的许可证费用。 所有用户,不论是个人还是企业,都必须遵守寄养商业许可证条款。"
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "试用期信息",
|
"title": "试用期信息",
|
||||||
|
|
@ -2881,7 +2881,7 @@
|
||||||
"httpDestFormatJsonArrayTitle": "JSON 数组",
|
"httpDestFormatJsonArrayTitle": "JSON 数组",
|
||||||
"httpDestFormatJsonArrayDescription": "每批一个请求,实体是一个 JSON 数组。与大多数通用的 Web 钩子和数据兼容。",
|
"httpDestFormatJsonArrayDescription": "每批一个请求,实体是一个 JSON 数组。与大多数通用的 Web 钩子和数据兼容。",
|
||||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||||
"httpDestFormatNdjsonDescription": "每批有一个请求,物体是换行符限制的 JSON ——每行一个对象,不是外部数组。 Sluk HEC、Elastic / OpenSearch和Grafana Loki所需。",
|
"httpDestFormatNdjsonDescription": "每批有一个请求,物体是换行符限制的 JSON --每行一个对象,不是外部数组。 Sluk HEC、Elastic / OpenSearch和Grafana Loki所需。",
|
||||||
"httpDestFormatSingleTitle": "每个请求一个事件",
|
"httpDestFormatSingleTitle": "每个请求一个事件",
|
||||||
"httpDestFormatSingleDescription": "为每个事件单独发送一个 HTTP POST。仅用于无法处理批量的端点。",
|
"httpDestFormatSingleDescription": "为每个事件单独发送一个 HTTP POST。仅用于无法处理批量的端点。",
|
||||||
"httpDestLogTypesTitle": "日志类型",
|
"httpDestLogTypesTitle": "日志类型",
|
||||||
|
|
|
||||||
|
|
@ -1763,7 +1763,7 @@
|
||||||
"description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能",
|
"description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能",
|
||||||
"introTitle": "託管式自架 Pangolin",
|
"introTitle": "託管式自架 Pangolin",
|
||||||
"introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。",
|
"introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。",
|
||||||
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 — — 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
|
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 - - 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
|
||||||
"benefitSimplerOperations": {
|
"benefitSimplerOperations": {
|
||||||
"title": "簡單的操作",
|
"title": "簡單的操作",
|
||||||
"description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。"
|
"description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。"
|
||||||
|
|
@ -2035,7 +2035,7 @@
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"commercialUseDisclosure": {
|
"commercialUseDisclosure": {
|
||||||
"title": "使用情況披露",
|
"title": "使用情況披露",
|
||||||
"description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境——需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。"
|
"description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境--需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。"
|
||||||
},
|
},
|
||||||
"trialPeriodInformation": {
|
"trialPeriodInformation": {
|
||||||
"title": "試用期資訊",
|
"title": "試用期資訊",
|
||||||
|
|
|
||||||
|
|
@ -189,9 +189,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||||
targetId: integer("targetId").references(() => targets.targetId, {
|
targetId: integer("targetId").references(() => targets.targetId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
orgId: varchar("orgId").references(() => orgs.orgId, {
|
orgId: varchar("orgId")
|
||||||
onDelete: "cascade"
|
.references(() => orgs.orgId, {
|
||||||
}),
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
|
|
|
||||||
|
|
@ -209,11 +209,14 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||||
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
targetId: integer("targetId")
|
targetId: integer("targetId").references(() => targets.targetId, {
|
||||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
|
||||||
orgId: text("orgId").references(() => orgs.orgId, {
|
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|
@ -294,7 +297,7 @@ export const siteResources = sqliteTable("siteResources", {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
subdomain: text("subdomain"),
|
subdomain: text("subdomain"),
|
||||||
fullDomain: text("fullDomain"),
|
fullDomain: text("fullDomain")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const networks = sqliteTable("networks", {
|
export const networks = sqliteTable("networks", {
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ function formatDataItems(
|
||||||
.replace(/([A-Z])/g, " $1")
|
.replace(/([A-Z])/g, " $1")
|
||||||
.replace(/^./, (s) => s.toUpperCase())
|
.replace(/^./, (s) => s.toUpperCase())
|
||||||
.trim(),
|
.trim(),
|
||||||
value: String(value ?? "—")
|
value: String(value ?? "-")
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,4 +137,4 @@ export const AlertNotification = ({ eventType, orgId, data }: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AlertNotification;
|
export default AlertNotification;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||||
}: EnterpriseEditionKeyGeneratedProps) => {
|
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||||
const previewText = personalUseOnly
|
const previewText = personalUseOnly
|
||||||
? "Your Enterprise Edition key for personal use is ready"
|
? "Your Enterprise Edition key for personal use is ready"
|
||||||
: "Thank you for your purchase — your Enterprise Edition key is ready";
|
: "Thank you for your purchase - your Enterprise Edition key is ready";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
|
|
|
||||||
|
|
@ -825,7 +825,7 @@ async function handleSubnetProxyTargetUpdates(
|
||||||
// Check if this client still has access to another resource
|
// Check if this client still has access to another resource
|
||||||
// on this specific site with the same destination. We scope
|
// on this specific site with the same destination. We scope
|
||||||
// by siteId (via siteNetworks) rather than networkId because
|
// by siteId (via siteNetworks) rather than networkId because
|
||||||
// removePeerData operates per-site — a resource on a different
|
// removePeerData operates per-site - a resource on a different
|
||||||
// site sharing the same network should not block removal here.
|
// site sharing the same network should not block removal here.
|
||||||
const destinationStillInUse = await trx
|
const destinationStillInUse = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -980,7 +980,7 @@ export async function rebuildClientAssociationsFromClient(
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Group by siteId for site-level associations — look up via siteNetworks since
|
// Group by siteId for site-level associations - look up via siteNetworks since
|
||||||
// siteResources no longer carries a direct siteId column.
|
// siteResources no longer carries a direct siteId column.
|
||||||
const networkIds = Array.from(
|
const networkIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
|
|
@ -1459,7 +1459,7 @@ async function handleMessagesForClientResources(
|
||||||
// Check if this client still has access to another resource
|
// Check if this client still has access to another resource
|
||||||
// on this specific site with the same destination. We scope
|
// on this specific site with the same destination. We scope
|
||||||
// by siteId (via siteNetworks) rather than networkId because
|
// by siteId (via siteNetworks) rather than networkId because
|
||||||
// removePeerData operates per-site — a resource on a different
|
// removePeerData operates per-site - a resource on a different
|
||||||
// site sharing the same network should not block removal here.
|
// site sharing the same network should not block removal here.
|
||||||
const destinationStillInUse = await trx
|
const destinationStillInUse = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|
|
||||||
|
|
@ -1011,7 +1011,7 @@ export class TraefikConfigManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isUnused) {
|
if (!isUnused) {
|
||||||
// Domain is still active — remove from pending deletion if it was queued
|
// Domain is still active - remove from pending deletion if it was queued
|
||||||
if (this.pendingDeletion.has(dirName)) {
|
if (this.pendingDeletion.has(dirName)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Certificate ${dirName} is active again, cancelling pending deletion`
|
`Certificate ${dirName} is active again, cancelling pending deletion`
|
||||||
|
|
@ -1021,7 +1021,7 @@ export class TraefikConfigManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Domain is unused — add to pending deletion or decrement its counter
|
// Domain is unused - add to pending deletion or decrement its counter
|
||||||
if (!this.pendingDeletion.has(dirName)) {
|
if (!this.pendingDeletion.has(dirName)) {
|
||||||
const graceCycles = 3;
|
const graceCycles = 3;
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -1036,7 +1036,7 @@ export class TraefikConfigManager {
|
||||||
);
|
);
|
||||||
this.pendingDeletion.set(dirName, remaining);
|
this.pendingDeletion.set(dirName, remaining);
|
||||||
} else {
|
} else {
|
||||||
// Grace period expired — actually delete now
|
// Grace period expired - actually delete now
|
||||||
this.pendingDeletion.delete(dirName);
|
this.pendingDeletion.delete(dirName);
|
||||||
|
|
||||||
const domainDir = path.join(certsPath, dirName);
|
const domainDir = path.join(certsPath, dirName);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function encodePath(path: string | null | undefined): string {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exact replica of the OLD key computation from upstream main.
|
* Exact replica of the OLD key computation from upstream main.
|
||||||
* Uses sanitize() for paths — this is what had the collision bug.
|
* Uses sanitize() for paths - this is what had the collision bug.
|
||||||
*/
|
*/
|
||||||
function oldKeyComputation(
|
function oldKeyComputation(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
|
|
@ -44,7 +44,7 @@ function oldKeyComputation(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replica of the NEW key computation from our fix.
|
* Replica of the NEW key computation from our fix.
|
||||||
* Uses encodePath() for paths — collision-free.
|
* Uses encodePath() for paths - collision-free.
|
||||||
*/
|
*/
|
||||||
function newKeyComputation(
|
function newKeyComputation(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
|
|
@ -195,11 +195,11 @@ function runTests() {
|
||||||
true,
|
true,
|
||||||
"/a/b and /a-b MUST have different keys"
|
"/a/b and /a-b MUST have different keys"
|
||||||
);
|
);
|
||||||
console.log(" PASS: collision fix — /a/b vs /a-b have different keys");
|
console.log(" PASS: collision fix - /a/b vs /a-b have different keys");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 9: demonstrate the old bug — old code maps /a/b and /a-b to same key
|
// Test 9: demonstrate the old bug - old code maps /a/b and /a-b to same key
|
||||||
{
|
{
|
||||||
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
||||||
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
||||||
|
|
@ -208,11 +208,11 @@ function runTests() {
|
||||||
oldKeyDash,
|
oldKeyDash,
|
||||||
"old code MUST have this collision (confirms the bug exists)"
|
"old code MUST have this collision (confirms the bug exists)"
|
||||||
);
|
);
|
||||||
console.log(" PASS: confirmed old code bug — /a/b and /a-b collided");
|
console.log(" PASS: confirmed old code bug - /a/b and /a-b collided");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 10: /api/v1 and /api-v1 — old code collision, new code fixes it
|
// Test 10: /api/v1 and /api-v1 - old code collision, new code fixes it
|
||||||
{
|
{
|
||||||
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||||
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||||
|
|
@ -229,11 +229,11 @@ function runTests() {
|
||||||
true,
|
true,
|
||||||
"new code must separate /api/v1 and /api-v1"
|
"new code must separate /api/v1 and /api-v1"
|
||||||
);
|
);
|
||||||
console.log(" PASS: collision fix — /api/v1 vs /api-v1");
|
console.log(" PASS: collision fix - /api/v1 vs /api-v1");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 11: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed
|
// Test 11: /app.v2 and /app/v2 and /app-v2 - three-way collision fixed
|
||||||
{
|
{
|
||||||
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
||||||
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
||||||
|
|
@ -245,14 +245,14 @@ function runTests() {
|
||||||
"three paths must produce three unique keys"
|
"three paths must produce three unique keys"
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
" PASS: collision fix — three-way /app.v2, /app/v2, /app-v2"
|
" PASS: collision fix - three-way /app.v2, /app/v2, /app-v2"
|
||||||
);
|
);
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Edge cases ───────────────────────────────────────────────────
|
// ── Edge cases ───────────────────────────────────────────────────
|
||||||
|
|
||||||
// Test 12: same path in different resources — always separate
|
// Test 12: same path in different resources - always separate
|
||||||
{
|
{
|
||||||
const key1 = newKeyComputation(1, "/api", "prefix", null, null);
|
const key1 = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
const key2 = newKeyComputation(2, "/api", "prefix", null, null);
|
const key2 = newKeyComputation(2, "/api", "prefix", null, null);
|
||||||
|
|
@ -261,11 +261,11 @@ function runTests() {
|
||||||
true,
|
true,
|
||||||
"different resources with same path must have different keys"
|
"different resources with same path must have different keys"
|
||||||
);
|
);
|
||||||
console.log(" PASS: edge case — same path, different resources");
|
console.log(" PASS: edge case - same path, different resources");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 13: same resource, different pathMatchType — separate keys
|
// Test 13: same resource, different pathMatchType - separate keys
|
||||||
{
|
{
|
||||||
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
||||||
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
|
@ -274,11 +274,11 @@ function runTests() {
|
||||||
true,
|
true,
|
||||||
"exact vs prefix must have different keys"
|
"exact vs prefix must have different keys"
|
||||||
);
|
);
|
||||||
console.log(" PASS: edge case — same path, different match types");
|
console.log(" PASS: edge case - same path, different match types");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 14: same resource and path, different rewrite config — separate keys
|
// Test 14: same resource and path, different rewrite config - separate keys
|
||||||
{
|
{
|
||||||
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
const withRewrite = newKeyComputation(
|
const withRewrite = newKeyComputation(
|
||||||
|
|
@ -293,7 +293,7 @@ function runTests() {
|
||||||
true,
|
true,
|
||||||
"with vs without rewrite must have different keys"
|
"with vs without rewrite must have different keys"
|
||||||
);
|
);
|
||||||
console.log(" PASS: edge case — same path, different rewrite config");
|
console.log(" PASS: edge case - same path, different rewrite config");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +308,7 @@ function runTests() {
|
||||||
paths.length,
|
paths.length,
|
||||||
"special URL chars must produce unique keys"
|
"special URL chars must produce unique keys"
|
||||||
);
|
);
|
||||||
console.log(" PASS: edge case — special URL characters in paths");
|
console.log(" PASS: edge case - special URL characters in paths");
|
||||||
passed++;
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ async function pushCertUpdateToAffectedNewts(
|
||||||
await cache.del(`cert:${resource.fullDomain}`);
|
await cache.del(`cert:${resource.fullDomain}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate target once — same cert applies to all sites for this resource
|
// Generate target once - same cert applies to all sites for this resource
|
||||||
const newTargets = await generateSubnetProxyTargetV2(
|
const newTargets = await generateSubnetProxyTargetV2(
|
||||||
resource,
|
resource,
|
||||||
resourceClients
|
resourceClients
|
||||||
|
|
@ -157,7 +157,7 @@ async function pushCertUpdateToAffectedNewts(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the old targets — same routing shape but with the previous cert/key.
|
// Construct the old targets - same routing shape but with the previous cert/key.
|
||||||
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
|
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
|
||||||
// semantics correct so the update message accurately reflects what changed.
|
// semantics correct so the update message accurately reflects what changed.
|
||||||
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
|
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ export async function flushConnectionLogToDb(): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop processing further batches from this snapshot — they will
|
// Stop processing further batches from this snapshot - they will
|
||||||
// be picked up via the re-queued records on the next flush.
|
// be picked up via the re-queued records on the next flush.
|
||||||
const remaining = snapshot.slice(i + INSERT_BATCH_SIZE);
|
const remaining = snapshot.slice(i + INSERT_BATCH_SIZE);
|
||||||
if (remaining.length > 0) {
|
if (remaining.length > 0) {
|
||||||
|
|
@ -180,7 +180,7 @@ const flushTimer = setInterval(async () => {
|
||||||
}, FLUSH_INTERVAL_MS);
|
}, FLUSH_INTERVAL_MS);
|
||||||
|
|
||||||
// Calling unref() means this timer will not keep the Node.js event loop alive
|
// Calling unref() means this timer will not keep the Node.js event loop alive
|
||||||
// on its own — the process can still exit normally when there is no other work
|
// on its own - the process can still exit normally when there is no other work
|
||||||
// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly
|
// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly
|
||||||
// before process.exit(), so no data is lost.
|
// before process.exit(), so no data is lost.
|
||||||
flushTimer.unref();
|
flushTimer.unref();
|
||||||
|
|
@ -223,7 +223,7 @@ export function logConnectionAudit(record: ConnectionLogRecord): void {
|
||||||
buffer.push(record);
|
buffer.push(record);
|
||||||
|
|
||||||
if (buffer.length >= MAX_BUFFERED_RECORDS) {
|
if (buffer.length >= MAX_BUFFERED_RECORDS) {
|
||||||
// Fire and forget — errors are handled inside flushConnectionLogToDb
|
// Fire and forget - errors are handled inside flushConnectionLogToDb
|
||||||
flushConnectionLogToDb().catch((error) => {
|
flushConnectionLogToDb().catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unexpected error during size-triggered connection log flush:",
|
"Unexpected error during size-triggered connection log flush:",
|
||||||
|
|
@ -231,4 +231,4 @@ export function logConnectionAudit(record: ConnectionLogRecord): void {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||||
*
|
*
|
||||||
* **Payload formats** (controlled by `config.format`):
|
* **Payload formats** (controlled by `config.format`):
|
||||||
*
|
*
|
||||||
* - `json_array` (default) — one POST per batch, body is a JSON array:
|
* - `json_array` (default) - one POST per batch, body is a JSON array:
|
||||||
* ```json
|
* ```json
|
||||||
* [
|
* [
|
||||||
* { "event": "request", "timestamp": "2024-01-01T00:00:00.000Z", "data": { … } },
|
* { "event": "request", "timestamp": "2024-01-01T00:00:00.000Z", "data": { … } },
|
||||||
|
|
@ -46,7 +46,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||||
* ```
|
* ```
|
||||||
* `Content-Type: application/json`
|
* `Content-Type: application/json`
|
||||||
*
|
*
|
||||||
* - `ndjson` — one POST per batch, body is newline-delimited JSON (one object
|
* - `ndjson` - one POST per batch, body is newline-delimited JSON (one object
|
||||||
* per line, no outer array). Required by Splunk HEC, Elastic/OpenSearch,
|
* per line, no outer array). Required by Splunk HEC, Elastic/OpenSearch,
|
||||||
* and Grafana Loki:
|
* and Grafana Loki:
|
||||||
* ```
|
* ```
|
||||||
|
|
@ -55,7 +55,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||||
* ```
|
* ```
|
||||||
* `Content-Type: application/x-ndjson`
|
* `Content-Type: application/x-ndjson`
|
||||||
*
|
*
|
||||||
* - `json_single` — one POST **per event**, body is a plain JSON object.
|
* - `json_single` - one POST **per event**, body is a plain JSON object.
|
||||||
* Use only for endpoints that cannot handle batches at all.
|
* Use only for endpoints that cannot handle batches at all.
|
||||||
*
|
*
|
||||||
* With a body template each event is rendered through the template before
|
* With a body template each event is rendered through the template before
|
||||||
|
|
@ -319,4 +319,4 @@ function epochSecondsToIso(epochSeconds: number): string {
|
||||||
function escapeJsonString(value: string): string {
|
function escapeJsonString(value: string): string {
|
||||||
// JSON.stringify produces `"<escaped>"` – strip the outer quotes.
|
// JSON.stringify produces `"<escaped>"` – strip the outer quotes.
|
||||||
return JSON.stringify(value).slice(1, -1);
|
return JSON.stringify(value).slice(1, -1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ export type AuthType = "none" | "bearer" | "basic" | "custom";
|
||||||
/**
|
/**
|
||||||
* Controls how the batch of events is serialised into the HTTP request body.
|
* Controls how the batch of events is serialised into the HTTP request body.
|
||||||
*
|
*
|
||||||
* - `json_array` – `[{…}, {…}]` — default; one POST per batch wrapped in a
|
* - `json_array` – `[{…}, {…}]` - default; one POST per batch wrapped in a
|
||||||
* JSON array. Works with most generic webhooks and Datadog.
|
* JSON array. Works with most generic webhooks and Datadog.
|
||||||
* - `ndjson` – `{…}\n{…}` — newline-delimited JSON, one object per
|
* - `ndjson` – `{…}\n{…}` - newline-delimited JSON, one object per
|
||||||
* line. Required by Splunk HEC, Elastic/OpenSearch, Loki.
|
* line. Required by Splunk HEC, Elastic/OpenSearch, Loki.
|
||||||
* - `json_single` – one HTTP POST per event, body is a plain JSON object.
|
* - `json_single` – one HTTP POST per event, body is a plain JSON object.
|
||||||
* Use only for endpoints that cannot handle batches at all.
|
* Use only for endpoints that cannot handle batches at all.
|
||||||
|
|
@ -131,4 +131,4 @@ export interface DestinationFailureState {
|
||||||
nextRetryAt: number;
|
nextRetryAt: number;
|
||||||
/** Date.now() value of the very first failure in the current streak */
|
/** Date.now() value of the very first failure in the current streak */
|
||||||
firstFailedAt: number;
|
firstFailedAt: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ export async function getTraefikConfig(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query siteResources in HTTP mode with SSL enabled and aliases — cert generation / HTTPS edge
|
// Query siteResources in HTTP mode with SSL enabled and aliases - cert generation / HTTPS edge
|
||||||
const siteResourcesWithFullDomain = await db
|
const siteResourcesWithFullDomain = await db
|
||||||
.select({
|
.select({
|
||||||
siteResourceId: siteResources.siteResourceId,
|
siteResourceId: siteResources.siteResourceId,
|
||||||
|
|
@ -1010,7 +1010,7 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPS router — presence of this entry triggers cert generation
|
// HTTPS router - presence of this entry triggers cert generation
|
||||||
config_output.http.routers[siteResourceRouterName] = {
|
config_output.http.routers[siteResourceRouterName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
config.getRawConfig().traefik.https_entrypoint
|
config.getRawConfig().traefik.https_entrypoint
|
||||||
|
|
@ -1022,7 +1022,7 @@ export async function getTraefikConfig(
|
||||||
tls
|
tls
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assets bypass router — lets Next.js static files load without rewrite
|
// Assets bypass router - lets Next.js static files load without rewrite
|
||||||
config_output.http.routers[`${siteResourceRouterName}-assets`] = {
|
config_output.http.routers[`${siteResourceRouterName}-assets`] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
config.getRawConfig().traefik.https_entrypoint
|
config.getRawConfig().traefik.https_entrypoint
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck, targets, resources } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
|
|
@ -84,12 +84,36 @@ export async function listHealthChecks(
|
||||||
|
|
||||||
const whereClause = and(
|
const whereClause = and(
|
||||||
eq(targetHealthCheck.orgId, orgId),
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
isNull(targetHealthCheck.targetId)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = await db
|
const list = await db
|
||||||
.select()
|
.select({
|
||||||
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
|
name: targetHealthCheck.name,
|
||||||
|
hcEnabled: targetHealthCheck.hcEnabled,
|
||||||
|
hcHealth: targetHealthCheck.hcHealth,
|
||||||
|
hcMode: targetHealthCheck.hcMode,
|
||||||
|
hcHostname: targetHealthCheck.hcHostname,
|
||||||
|
hcPort: targetHealthCheck.hcPort,
|
||||||
|
hcPath: targetHealthCheck.hcPath,
|
||||||
|
hcScheme: targetHealthCheck.hcScheme,
|
||||||
|
hcMethod: targetHealthCheck.hcMethod,
|
||||||
|
hcInterval: targetHealthCheck.hcInterval,
|
||||||
|
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
|
||||||
|
hcTimeout: targetHealthCheck.hcTimeout,
|
||||||
|
hcHeaders: targetHealthCheck.hcHeaders,
|
||||||
|
hcFollowRedirects: targetHealthCheck.hcFollowRedirects,
|
||||||
|
hcStatus: targetHealthCheck.hcStatus,
|
||||||
|
hcTlsServerName: targetHealthCheck.hcTlsServerName,
|
||||||
|
hcHealthyThreshold: targetHealthCheck.hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold: targetHealthCheck.hcUnhealthyThreshold,
|
||||||
|
resourceId: resources.resourceId,
|
||||||
|
resourceName: resources.name,
|
||||||
|
resourceNiceId: resources.niceId
|
||||||
|
})
|
||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
|
.leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId))
|
||||||
|
.leftJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
.where(whereClause)
|
.where(whereClause)
|
||||||
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
@ -124,7 +148,10 @@ export async function listHealthChecks(
|
||||||
hcStatus: row.hcStatus ?? null,
|
hcStatus: row.hcStatus ?? null,
|
||||||
hcTlsServerName: row.hcTlsServerName ?? null,
|
hcTlsServerName: row.hcTlsServerName ?? null,
|
||||||
hcHealthyThreshold: row.hcHealthyThreshold ?? null,
|
hcHealthyThreshold: row.hcHealthyThreshold ?? null,
|
||||||
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null
|
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null,
|
||||||
|
resourceId: row.resourceId ?? null,
|
||||||
|
resourceName: row.resourceName ?? null,
|
||||||
|
resourceNiceId: row.resourceNiceId ?? null
|
||||||
})),
|
})),
|
||||||
pagination: {
|
pagination: {
|
||||||
total: count,
|
total: count,
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,11 @@ async function dbQueryRows<T extends Record<string, unknown>>(
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const anyDb = db as any;
|
const anyDb = db as any;
|
||||||
if (typeof anyDb.execute === "function") {
|
if (typeof anyDb.execute === "function") {
|
||||||
// PostgreSQL (node-postgres via Drizzle) — returns { rows: [...] } or an array
|
// PostgreSQL (node-postgres via Drizzle) - returns { rows: [...] } or an array
|
||||||
const result = await anyDb.execute(query);
|
const result = await anyDb.execute(query);
|
||||||
return (Array.isArray(result) ? result : (result.rows ?? [])) as T[];
|
return (Array.isArray(result) ? result : (result.rows ?? [])) as T[];
|
||||||
}
|
}
|
||||||
// SQLite (better-sqlite3 via Drizzle) — returns an array directly
|
// SQLite (better-sqlite3 via Drizzle) - returns an array directly
|
||||||
return (await anyDb.all(query)) as T[];
|
return (await anyDb.all(query)) as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ function isSQLite(): boolean {
|
||||||
* Swaps out the accumulator before writing so that any bandwidth messages
|
* Swaps out the accumulator before writing so that any bandwidth messages
|
||||||
* received during the flush are captured in the new accumulator rather than
|
* received during the flush are captured in the new accumulator rather than
|
||||||
* being lost or causing contention. Sites are updated in chunks via a single
|
* being lost or causing contention. Sites are updated in chunks via a single
|
||||||
* batch UPDATE per chunk. Failed chunks are discarded — exact per-flush
|
* batch UPDATE per chunk. Failed chunks are discarded - exact per-flush
|
||||||
* accuracy is not critical and re-queuing is not worth the added complexity.
|
* accuracy is not critical and re-queuing is not worth the added complexity.
|
||||||
*
|
*
|
||||||
* This function is exported so that the application's graceful-shutdown
|
* This function is exported so that the application's graceful-shutdown
|
||||||
|
|
@ -125,7 +125,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||||
const currentTime = new Date().toISOString();
|
const currentTime = new Date().toISOString();
|
||||||
|
|
||||||
// Sort by publicKey for consistent lock ordering across concurrent
|
// Sort by publicKey for consistent lock ordering across concurrent
|
||||||
// writers — deadlock-prevention strategy.
|
// writers - deadlock-prevention strategy.
|
||||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
||||||
a.localeCompare(b)
|
a.localeCompare(b)
|
||||||
);
|
);
|
||||||
|
|
@ -150,7 +150,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
rows = await withDeadlockRetry(async () => {
|
rows = await withDeadlockRetry(async () => {
|
||||||
if (isSQLite()) {
|
if (isSQLite()) {
|
||||||
// SQLite: one UPDATE per row — no need for batch efficiency here.
|
// SQLite: one UPDATE per row - no need for batch efficiency here.
|
||||||
const results: { orgId: string; pubKey: string }[] = [];
|
const results: { orgId: string; pubKey: string }[] = [];
|
||||||
for (const [publicKey, { bytesIn, bytesOut }] of chunk) {
|
for (const [publicKey, { bytesIn, bytesOut }] of chunk) {
|
||||||
const result = await dbQueryRows<{
|
const result = await dbQueryRows<{
|
||||||
|
|
@ -170,7 +170,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
|
// PostgreSQL: batch UPDATE … FROM (VALUES …) - single round-trip per chunk.
|
||||||
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
|
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
|
||||||
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
|
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
|
||||||
);
|
);
|
||||||
|
|
@ -191,7 +191,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||||
`Failed to flush bandwidth chunk [${i}–${chunkEnd}], discarding ${chunk.length} site(s):`,
|
`Failed to flush bandwidth chunk [${i}–${chunkEnd}], discarding ${chunk.length} site(s):`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
// Discard the chunk — exact per-flush accuracy is not critical.
|
// Discard the chunk - exact per-flush accuracy is not critical.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||||
totalBandwidth
|
totalBandwidth
|
||||||
);
|
);
|
||||||
if (bandwidthUsage) {
|
if (bandwidthUsage) {
|
||||||
// Fire-and-forget — don't block the flush on limit checking.
|
// Fire-and-forget - don't block the flush on limit checking.
|
||||||
usageService
|
usageService
|
||||||
.checkLimitSet(
|
.checkLimitSet(
|
||||||
orgId,
|
orgId,
|
||||||
|
|
@ -298,7 +298,7 @@ export async function updateSiteBandwidth(
|
||||||
exitNodeId?: number
|
exitNodeId?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
||||||
// Skip peers that haven't transferred any data — writing zeros to the
|
// Skip peers that haven't transferred any data - writing zeros to the
|
||||||
// database would be a no-op anyway.
|
// database would be a no-op anyway.
|
||||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
if (bytesIn <= 0 && bytesOut <= 0) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ export type ListHealthChecksResponse = {
|
||||||
hcTlsServerName: string | null;
|
hcTlsServerName: string | null;
|
||||||
hcHealthyThreshold: number | null;
|
hcHealthyThreshold: number | null;
|
||||||
hcUnhealthyThreshold: number | null;
|
hcUnhealthyThreshold: number | null;
|
||||||
|
resourceId: number | null;
|
||||||
|
resourceName: string | null;
|
||||||
|
resourceNiceId: string | null;
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ export async function flushBandwidthToDb(): Promise<void> {
|
||||||
const currentTime = new Date().toISOString();
|
const currentTime = new Date().toISOString();
|
||||||
|
|
||||||
// Sort by publicKey for consistent lock ordering across concurrent
|
// Sort by publicKey for consistent lock ordering across concurrent
|
||||||
// writers — this is the same deadlock-prevention strategy used in the
|
// writers - this is the same deadlock-prevention strategy used in the
|
||||||
// original per-message implementation.
|
// original per-message implementation.
|
||||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
||||||
a.localeCompare(b)
|
a.localeCompare(b)
|
||||||
|
|
@ -143,7 +143,7 @@ const flushTimer = setInterval(async () => {
|
||||||
}, FLUSH_INTERVAL_MS);
|
}, FLUSH_INTERVAL_MS);
|
||||||
|
|
||||||
// Calling unref() means this timer will not keep the Node.js event loop alive
|
// Calling unref() means this timer will not keep the Node.js event loop alive
|
||||||
// on its own — the process can still exit normally when there is no other work
|
// on its own - the process can still exit normally when there is no other work
|
||||||
// left. The graceful-shutdown path (see server/cleanup.ts) will call
|
// left. The graceful-shutdown path (see server/cleanup.ts) will call
|
||||||
// flushBandwidthToDb() explicitly before process.exit(), so no data is lost.
|
// flushBandwidthToDb() explicitly before process.exit(), so no data is lost.
|
||||||
flushTimer.unref();
|
flushTimer.unref();
|
||||||
|
|
@ -167,7 +167,7 @@ export const handleReceiveBandwidthMessage: MessageHandler = async (
|
||||||
// Accumulate the incoming data in memory; the periodic timer (and the
|
// Accumulate the incoming data in memory; the periodic timer (and the
|
||||||
// shutdown hook) will take care of writing it to the database.
|
// shutdown hook) will take care of writing it to the database.
|
||||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
||||||
// Skip peers that haven't transferred any data — writing zeros to the
|
// Skip peers that haven't transferred any data - writing zeros to the
|
||||||
// database would be a no-op anyway.
|
// database would be a no-op anyway.
|
||||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
if (bytesIn <= 0 && bytesOut <= 0) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const OFFLINE_THRESHOLD_BANDWIDTH_MS = 8 * 60 * 1000; // 8 minutes
|
||||||
* Starts the background interval that checks for newt sites that haven't
|
* Starts the background interval that checks for newt sites that haven't
|
||||||
* pinged recently and marks them as offline. For backward compatibility,
|
* pinged recently and marks them as offline. For backward compatibility,
|
||||||
* a site is only marked offline when there is no active WebSocket connection
|
* a site is only marked offline when there is no active WebSocket connection
|
||||||
* either — so older newt versions that don't send pings but remain connected
|
* either - so older newt versions that don't send pings but remain connected
|
||||||
* continue to be treated as online.
|
* continue to be treated as online.
|
||||||
*/
|
*/
|
||||||
export const startNewtOfflineChecker = (): void => {
|
export const startNewtOfflineChecker = (): void => {
|
||||||
|
|
@ -63,7 +63,7 @@ export const startNewtOfflineChecker = (): void => {
|
||||||
);
|
);
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online`
|
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket - keeping site ${staleSite.siteId} online`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newlyOnlineSites = await withRetry(async () => {
|
const newlyOnlineSites = await withRetry(async () => {
|
||||||
// Only update sites that were offline — these are the
|
// Only update sites that were offline - these are the
|
||||||
// offline→online transitions. .returning() gives us exactly
|
// offline→online transitions. .returning() gives us exactly
|
||||||
// the site IDs that changed state.
|
// the site IDs that changed state.
|
||||||
const transitioned = await db
|
const transitioned = await db
|
||||||
|
|
@ -249,7 +249,7 @@ async function flushClientPingsToDb(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush everything — called by the interval timer and during shutdown.
|
* Flush everything - called by the interval timer and during shutdown.
|
||||||
*/
|
*/
|
||||||
export async function flushPingsToDb(): Promise<void> {
|
export async function flushPingsToDb(): Promise<void> {
|
||||||
await flushSitePingsToDb();
|
await flushSitePingsToDb();
|
||||||
|
|
@ -314,7 +314,7 @@ function isTransientError(error: any): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
|
// PostgreSQL deadlock detected - always safe to retry (one winner guaranteed)
|
||||||
if (code === "40P01" || message.includes("deadlock")) {
|
if (code === "40P01" || message.includes("deadlock")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ export async function registerNewt(
|
||||||
dateCreated: moment().toISOString()
|
dateCreated: moment().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Consume the provisioning key — cascade removes siteProvisioningKeyOrg
|
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
|
||||||
await trx
|
await trx
|
||||||
.update(siteProvisioningKeys)
|
.update(siteProvisioningKeys)
|
||||||
.set({
|
.set({
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the peer add handshake — if the peer was already added this will be a no-op
|
// Trigger the peer add handshake - if the peer was already added this will be a no-op
|
||||||
await initPeerAddHandshake(
|
await initPeerAddHandshake(
|
||||||
client.clientId,
|
client.clientId,
|
||||||
{
|
{
|
||||||
|
|
@ -236,4 +236,4 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ export async function createTarget(
|
||||||
healthCheck = await db
|
healthCheck = await db
|
||||||
.insert(targetHealthCheck)
|
.insert(targetHealthCheck)
|
||||||
.values({
|
.values({
|
||||||
name: `${targetData.ip}:${targetData.port}`,
|
orgId: resource.orgId,
|
||||||
targetId: newTarget[0].targetId,
|
targetId: newTarget[0].targetId,
|
||||||
hcEnabled: targetData.hcEnabled ?? false,
|
hcEnabled: targetData.hcEnabled ?? false,
|
||||||
hcPath: targetData.hcPath ?? null,
|
hcPath: targetData.hcPath ?? null,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
||||||
"ws/round-trip/complete": handleRoundTripMessage
|
"ws/round-trip/complete": handleRoundTripMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start the ping accumulator for all builds — it batches per-site online/lastPing
|
// Start the ping accumulator for all builds - it batches per-site online/lastPing
|
||||||
// updates into periodic bulk writes, preventing connection pool exhaustion.
|
// updates into periodic bulk writes, preventing connection pool exhaustion.
|
||||||
startPingAccumulator();
|
startPingAccumulator();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
function formatBytes(bytes: number | null): string {
|
function formatBytes(bytes: number | null): string {
|
||||||
if (bytes === null || bytes === undefined) return "—";
|
if (bytes === null || bytes === undefined) return "-";
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
|
@ -33,7 +33,7 @@ function formatBytes(bytes: number | null): string {
|
||||||
function formatDuration(startedAt: number, endedAt: number | null): string {
|
function formatDuration(startedAt: number, endedAt: number | null): string {
|
||||||
if (endedAt === null || endedAt === undefined) return "Active";
|
if (endedAt === null || endedAt === undefined) return "Active";
|
||||||
const durationSec = endedAt - startedAt;
|
const durationSec = endedAt - startedAt;
|
||||||
if (durationSec < 0) return "—";
|
if (durationSec < 0) return "-";
|
||||||
if (durationSec < 60) return `${durationSec}s`;
|
if (durationSec < 60) return `${durationSec}s`;
|
||||||
if (durationSec < 3600) {
|
if (durationSec < 3600) {
|
||||||
const m = Math.floor(durationSec / 60);
|
const m = Math.floor(durationSec / 60);
|
||||||
|
|
@ -460,7 +460,7 @@ export default function ConnectionLogsPage() {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{row.original.resourceName ?? "—"}
|
{row.original.resourceName ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -503,7 +503,7 @@ export default function ConnectionLogsPage() {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{row.original.clientName ?? "—"}
|
{row.original.clientName ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -538,7 +538,7 @@ export default function ConnectionLogsPage() {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <span>—</span>;
|
return <span>-</span>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -612,23 +612,23 @@ export default function ConnectionLogsPage() {
|
||||||
<div>
|
<div>
|
||||||
<strong>Session ID:</strong>{" "}
|
<strong>Session ID:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{row.sessionId ?? "—"}
|
{row.sessionId ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Protocol:</strong>{" "}
|
<strong>Protocol:</strong>{" "}
|
||||||
{row.protocol?.toUpperCase() ?? "—"}
|
{row.protocol?.toUpperCase() ?? "-"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Source:</strong>{" "}
|
<strong>Source:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{row.sourceAddr ?? "—"}
|
{row.sourceAddr ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Destination:</strong>{" "}
|
<strong>Destination:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{row.destAddr ?? "—"}
|
{row.destAddr ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -638,7 +638,7 @@ export default function ConnectionLogsPage() {
|
||||||
</div>*/}
|
</div>*/}
|
||||||
{/*<div>
|
{/*<div>
|
||||||
<strong>Resource:</strong>{" "}
|
<strong>Resource:</strong>{" "}
|
||||||
{row.resourceName ?? "—"}
|
{row.resourceName ?? "-"}
|
||||||
{row.resourceNiceId && (
|
{row.resourceNiceId && (
|
||||||
<span className="text-muted-foreground ml-1">
|
<span className="text-muted-foreground ml-1">
|
||||||
({row.resourceNiceId})
|
({row.resourceNiceId})
|
||||||
|
|
@ -646,7 +646,7 @@ export default function ConnectionLogsPage() {
|
||||||
)}
|
)}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
<div>
|
<div>
|
||||||
<strong>Site:</strong> {row.siteName ?? "—"}
|
<strong>Site:</strong> {row.siteName ?? "-"}
|
||||||
{row.siteNiceId && (
|
{row.siteNiceId && (
|
||||||
<span className="text-muted-foreground ml-1">
|
<span className="text-muted-foreground ml-1">
|
||||||
({row.siteNiceId})
|
({row.siteNiceId})
|
||||||
|
|
@ -654,7 +654,7 @@ export default function ConnectionLogsPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Site ID:</strong> {row.siteId ?? "—"}
|
<strong>Site ID:</strong> {row.siteId ?? "-"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Started At:</strong>{" "}
|
<strong>Started At:</strong>{" "}
|
||||||
|
|
@ -662,7 +662,7 @@ export default function ConnectionLogsPage() {
|
||||||
? new Date(
|
? new Date(
|
||||||
row.startedAt * 1000
|
row.startedAt * 1000
|
||||||
).toLocaleString()
|
).toLocaleString()
|
||||||
: "—"}
|
: "-"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Ended At:</strong>{" "}
|
<strong>Ended At:</strong>{" "}
|
||||||
|
|
@ -676,7 +676,7 @@ export default function ConnectionLogsPage() {
|
||||||
</div>
|
</div>
|
||||||
{/*<div>
|
{/*<div>
|
||||||
<strong>Resource ID:</strong>{" "}
|
<strong>Resource ID:</strong>{" "}
|
||||||
{row.siteResourceId ?? "—"}
|
{row.siteResourceId ?? "-"}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
|
||||||
|
|
@ -434,7 +434,7 @@ export default function StreamingDestinationsPage() {
|
||||||
disabled={!isEnterprise}
|
disabled={!isEnterprise}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* Add card is always clickable — paywall is enforced inside the picker */}
|
{/* Add card is always clickable - paywall is enforced inside the picker */}
|
||||||
<AddDestinationCard onClick={openCreate} />
|
<AddDestinationCard onClick={openCreate} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -341,19 +341,6 @@ function ProxyResourceTargetsForm({
|
||||||
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.hcHealth || "unknown";
|
const status = row.original.hcHealth || "unknown";
|
||||||
const isEnabled = row.original.hcEnabled;
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case "healthy":
|
|
||||||
return "green";
|
|
||||||
case "unhealthy":
|
|
||||||
return "red";
|
|
||||||
case "unknown":
|
|
||||||
default:
|
|
||||||
return "secondary";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -367,19 +354,7 @@ function ProxyResourceTargetsForm({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
return (
|
||||||
switch (status) {
|
|
||||||
case "healthy":
|
|
||||||
return <CircleCheck className="w-3 h-3" />;
|
|
||||||
case "unhealthy":
|
|
||||||
return <CircleX className="w-3 h-3" />;
|
|
||||||
case "unknown":
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center w-full">
|
<div className="flex items-center justify-center w-full">
|
||||||
{row.original.siteType === "newt" ? (
|
{row.original.siteType === "newt" ? (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -390,12 +365,15 @@ function ProxyResourceTargetsForm({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
|
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : "text-neutral-500"}`}
|
||||||
>
|
>
|
||||||
<Settings className="h-4 w-4 text-foreground" />
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${status === "healthy" ? "bg-green-500" : status === "unhealthy" ? "bg-destructive" : "bg-neutral-500"}`}
|
||||||
|
></div>
|
||||||
{getStatusText(status)}
|
{getStatusText(status)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,7 @@ export default function Page() {
|
||||||
setRemoteExitNodeOptions(exitNodeOptions);
|
setRemoteExitNodeOptions(exitNodeOptions);
|
||||||
|
|
||||||
if (exitNodeOptions.length === 0) {
|
if (exitNodeOptions.length === 0) {
|
||||||
// No remote exit nodes available — remove local option and default to newt
|
// No remote exit nodes available - remove local option and default to newt
|
||||||
setTunnelTypes((prev: any) =>
|
setTunnelTypes((prev: any) =>
|
||||||
prev.filter((item: any) => item.id !== "local")
|
prev.filter((item: any) => item.id !== "local")
|
||||||
);
|
);
|
||||||
|
|
@ -434,7 +434,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch remote exit nodes:", error);
|
console.error("Failed to fetch remote exit nodes:", error);
|
||||||
// If fetch fails, no remote exit nodes available — remove local option and default to newt
|
// If fetch fails, no remote exit nodes available - remove local option and default to newt
|
||||||
setTunnelTypes((prev: any) =>
|
setTunnelTypes((prev: any) =>
|
||||||
prev.filter((item: any) => item.id !== "local")
|
prev.filter((item: any) => item.id !== "local")
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ function aggregateStatusDotClass(status: AggregateSitesStatus): string {
|
||||||
return "bg-yellow-500";
|
return "bg-yellow-500";
|
||||||
case "allOffline":
|
case "allOffline":
|
||||||
default:
|
default:
|
||||||
return "bg-gray-500";
|
return "bg-neutral-500";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ function ClientResourceSitesStatusCell({
|
||||||
"h-2 w-2 shrink-0 rounded-full",
|
"h-2 w-2 shrink-0 rounded-full",
|
||||||
isOnline
|
isOnline
|
||||||
? "bg-green-500"
|
? "bg-green-500"
|
||||||
: "bg-gray-500"
|
: "bg-neutral-500"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export default function ExitNodesTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ export type HealthCheckRow = {
|
||||||
hcTlsServerName: string | null;
|
hcTlsServerName: string | null;
|
||||||
hcHealthyThreshold: number | null;
|
hcHealthyThreshold: number | null;
|
||||||
hcUnhealthyThreshold: number | null;
|
hcUnhealthyThreshold: number | null;
|
||||||
|
resourceId: number | null;
|
||||||
|
resourceName: string | null;
|
||||||
|
resourceNiceId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HealthCheckCredenzaProps =
|
export type HealthCheckCredenzaProps =
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { HeadersInput } from "@app/components/HeadersInput";
|
import { HeadersInput } from "@app/components/HeadersInput";
|
||||||
import {
|
import {
|
||||||
|
|
@ -103,22 +104,27 @@ export function HealthCheckFormFields({
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("healthCheckStrategy")}</FormLabel>
|
<FormLabel>{t("healthCheckStrategy")}</FormLabel>
|
||||||
<Select
|
<FormControl>
|
||||||
onValueChange={(value) =>
|
<StrategySelect
|
||||||
handleChange("hcMode", value, field.onChange)
|
cols={2}
|
||||||
}
|
options={[
|
||||||
value={field.value}
|
{
|
||||||
>
|
id: "http",
|
||||||
<FormControl>
|
title: "HTTP",
|
||||||
<SelectTrigger>
|
description: "Validates connectivity and checks the HTTP response status."
|
||||||
<SelectValue />
|
},
|
||||||
</SelectTrigger>
|
{
|
||||||
</FormControl>
|
id: "tcp",
|
||||||
<SelectContent>
|
title: "TCP",
|
||||||
<SelectItem value="http">HTTP</SelectItem>
|
description: "Verifies TCP connectivity only, without inspecting the response."
|
||||||
<SelectItem value="tcp">TCP</SelectItem>
|
}
|
||||||
</SelectContent>
|
]}
|
||||||
</Select>
|
value={field.value}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleChange("hcMode", value, field.onChange)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,17 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import { ArrowUpDown, ArrowUpRight, MoreHorizontal } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Span } from "next/dist/trace";
|
import Link from "next/link";
|
||||||
|
|
||||||
type StandaloneHealthChecksTableProps = {
|
type StandaloneHealthChecksTableProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTarget(row: HealthCheckRow): string {
|
function formatTarget(row: HealthCheckRow): string {
|
||||||
if (!row.hcHostname) return "—";
|
if (!row.hcHostname) return "-";
|
||||||
if (row.hcMode === "tcp") {
|
if (row.hcMode === "tcp") {
|
||||||
if (!row.hcPort) return row.hcHostname;
|
if (!row.hcPort) return row.hcHostname;
|
||||||
return `${row.hcHostname}:${row.hcPort}`;
|
return `${row.hcHostname}:${row.hcPort}`;
|
||||||
|
|
@ -154,7 +154,7 @@ export default function HealthChecksTable({
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>
|
||||||
{row.original.hcMode?.toUpperCase() ?? "—"}
|
{row.original.hcMode?.toUpperCase() ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -166,6 +166,27 @@ export default function HealthChecksTable({
|
||||||
),
|
),
|
||||||
cell: ({ row }) => <span>{formatTarget(row.original)}</span>
|
cell: ({ row }) => <span>{formatTarget(row.original)}</span>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "resource",
|
||||||
|
friendlyName: "Resource",
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">Resource</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
if (!r.resourceId || !r.resourceName || !r.resourceNiceId) {
|
||||||
|
return <span className="text-neutral-400">-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{r.resourceName}
|
||||||
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "health",
|
id: "health",
|
||||||
friendlyName: t("standaloneHcColumnHealth"),
|
friendlyName: t("standaloneHcColumnHealth"),
|
||||||
|
|
@ -191,7 +212,7 @@ export default function HealthChecksTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{healthLabel.unknown}</span>
|
<span>{healthLabel.unknown}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default function LocaleSwitcherSelect({
|
||||||
});
|
});
|
||||||
// Persist locale to the database (fire-and-forget)
|
// Persist locale to the database (fire-and-forget)
|
||||||
api.post("/user/locale", { locale }).catch(() => {
|
api.post("/user/locale", { locale }).catch(() => {
|
||||||
// Silently ignore errors — cookie is already set as fallback
|
// Silently ignore errors - cookie is already set as fallback
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ export default function MachineClientsTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("disconnected")}</span>
|
<span>{t("disconnected")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export default function PendingSitesTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>Offline</span>
|
<span>Offline</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export default function SitesTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("offline")}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,7 @@ export default function UserDevicesTable({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||||
<span>{t("disconnected")}</span>
|
<span>{t("disconnected")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,9 @@ export const orgQueries = {
|
||||||
hcTlsServerName: string | null;
|
hcTlsServerName: string | null;
|
||||||
hcHealthyThreshold: number | null;
|
hcHealthyThreshold: number | null;
|
||||||
hcUnhealthyThreshold: number | null;
|
hcUnhealthyThreshold: number | null;
|
||||||
|
resourceId: number | null;
|
||||||
|
resourceName: string | null;
|
||||||
|
resourceNiceId: string | null;
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ export async function getUserLocale(): Promise<Locale> {
|
||||||
return cookieLocale as Locale;
|
return cookieLocale as Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cookie found — try to restore from user's saved locale in DB
|
// No cookie found - try to restore from user's saved locale in DB
|
||||||
try {
|
try {
|
||||||
const res = await internal.get("/user", await authCookieHeader());
|
const res = await internal.get("/user", await authCookieHeader());
|
||||||
const userLocale = res.data?.data?.locale;
|
const userLocale = res.data?.data?.locale;
|
||||||
if (userLocale && locales.includes(userLocale as Locale)) {
|
if (userLocale && locales.includes(userLocale as Locale)) {
|
||||||
// Try to cache in a cookie so subsequent requests skip the API
|
// Try to cache in a cookie so subsequent requests skip the API
|
||||||
// call. cookies().set() is only permitted in Server Actions and
|
// call. cookies().set() is only permitted in Server Actions and
|
||||||
// Route Handlers — not during rendering — so we isolate it so
|
// Route Handlers - not during rendering - so we isolate it so
|
||||||
// that a write failure doesn't prevent the locale from being
|
// that a write failure doesn't prevent the locale from being
|
||||||
// returned for the current request.
|
// returned for the current request.
|
||||||
try {
|
try {
|
||||||
|
|
@ -40,7 +40,7 @@ export async function getUserLocale(): Promise<Locale> {
|
||||||
return userLocale as Locale;
|
return userLocale as Locale;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// User not logged in or API unavailable — fall through
|
// User not logged in or API unavailable - fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerList = await headers();
|
const headerList = await headers();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue