mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-08 19:06:36 +00:00
Compare commits
4 Commits
crowdin_de
...
1.17.0-s.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce165bfd5 | ||
|
|
035644eaf7 | ||
|
|
16e7233a3e | ||
|
|
1f74e1b320 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @oschwartz10612 @miloschwartz
|
||||
@@ -86,8 +86,6 @@ entryPoints:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
middlewares:
|
||||
- crowdsec@file
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"setupCreate": "Creare l'organizzazione, il sito e le risorse",
|
||||
"headerAuthCompatibilityInfo": "Abilita questa funzionalità per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.",
|
||||
"headerAuthCompatibilityInfo": "Abilita questo per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.",
|
||||
"headerAuthCompatibility": "Compatibilità estesa",
|
||||
"setupNewOrg": "Nuova Organizzazione",
|
||||
"setupCreateOrg": "Crea Organizzazione",
|
||||
"setupCreateResources": "Crea Risorse",
|
||||
"setupOrgName": "Nome dell'Organizzazione",
|
||||
"setupOrgName": "Nome Dell'Organizzazione",
|
||||
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
||||
"orgId": "Id Organizzazione",
|
||||
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
||||
"setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.",
|
||||
"componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.",
|
||||
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
||||
"welcome": "Benvenuto su Pangolin!",
|
||||
"welcomeTo": "Benvenuto su Pangolin!",
|
||||
"welcome": "Benvenuti a Pangolin",
|
||||
"welcomeTo": "Benvenuto a",
|
||||
"componentsCreateOrg": "Crea un'organizzazione",
|
||||
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
|
||||
"componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.",
|
||||
@@ -27,7 +27,7 @@
|
||||
"inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.",
|
||||
"inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.",
|
||||
"inviteCreateUser": "Si prega di creare un account prima.",
|
||||
"goHome": "Vai alla Home",
|
||||
"goHome": "Vai A Home",
|
||||
"inviteLogInOtherUser": "Accedi come utente diverso",
|
||||
"createAnAccount": "Crea un account",
|
||||
"inviteNotAccepted": "Invito Non Accettato",
|
||||
@@ -51,7 +51,7 @@
|
||||
"edit": "Modifica",
|
||||
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
||||
"siteDelete": "Elimina Sito",
|
||||
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.",
|
||||
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli obiettivi associati al sito verranno rimossi.",
|
||||
"siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
|
||||
"siteManageSites": "Gestisci Siti",
|
||||
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
||||
@@ -75,9 +75,9 @@
|
||||
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
||||
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
||||
"toggle": "Attiva/disattiva",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"dockerCompose": "Composizione Docker",
|
||||
"dockerRun": "Corsa Docker",
|
||||
"siteLearnLocal": "I siti locali non effettuano il tunnel, per saperne di più",
|
||||
"siteLearnLocal": "I siti locali non tunnel, saperne di più",
|
||||
"siteConfirmCopy": "Ho copiato la configurazione",
|
||||
"searchSitesProgress": "Cerca siti...",
|
||||
"siteAdd": "Aggiungi Sito",
|
||||
@@ -88,29 +88,29 @@
|
||||
"operatingSystem": "Sistema Operativo",
|
||||
"commands": "Comandi",
|
||||
"recommended": "Consigliato",
|
||||
"siteNewtDescription": "Per la migliore esperienza utente utilizzare Newt, che usa WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.",
|
||||
"siteNewtDescription": "Per la migliore esperienza utente, utilizzare Newt. Utilizza WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.",
|
||||
"siteRunsInDocker": "Esegue nel Docker",
|
||||
"siteRunsInShell": "Esegue in shell su macOS, Linux e Windows",
|
||||
"siteErrorDelete": "Errore nella eliminazione del sito",
|
||||
"siteErrorDelete": "Errore nell'eliminare il sito",
|
||||
"siteErrorUpdate": "Impossibile aggiornare il sito",
|
||||
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
||||
"siteUpdated": "Sito aggiornato",
|
||||
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
||||
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
||||
"siteSettingDescription": "Configura le impostazioni del sito",
|
||||
"siteSetting": "Impostazioni del sito {siteName}",
|
||||
"siteSetting": "Impostazioni {siteName}",
|
||||
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
||||
"siteWg": "WireGuard Base",
|
||||
"siteWgDescription": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteWgDescriptionSaas": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
||||
"siteSeeAll": "Vedi Tutti I Siti",
|
||||
"siteTunnelDescription": "Selezionare la modalità con la quale si desidera connettersi al sito",
|
||||
"siteTunnelDescription": "Determinare come si desidera connettersi al sito",
|
||||
"siteNewtCredentials": "Credenziali",
|
||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticherà con il server",
|
||||
"remoteNodeCredentialsDescription": "Questo è il modo in cui il nodo remoto si autenticherà con il server",
|
||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server",
|
||||
"remoteNodeCredentialsDescription": "Questo è come il nodo remoto si autenticherà con il server",
|
||||
"siteCredentialsSave": "Salva le credenziali",
|
||||
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
||||
"siteInfo": "Informazioni Sito",
|
||||
@@ -140,8 +140,8 @@
|
||||
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
||||
"shareTitleOptional": "Titolo (facoltativo)",
|
||||
"expireIn": "Scadenza In",
|
||||
"neverExpire": "Nessuna scadenza",
|
||||
"shareExpireDescription": "Il tempo di scadenza indica per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
|
||||
"neverExpire": "Mai scadere",
|
||||
"shareExpireDescription": "Il tempo di scadenza è per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
|
||||
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
|
||||
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
||||
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
||||
@@ -161,9 +161,9 @@
|
||||
"never": "Mai",
|
||||
"shareErrorSelectResource": "Seleziona una risorsa",
|
||||
"proxyResourceTitle": "Gestisci Risorse Pubbliche",
|
||||
"proxyResourceDescription": "Creare e gestire risorse pubbliche accessibili tramite un browser web",
|
||||
"proxyResourceDescription": "Creare e gestire risorse accessibili al pubblico tramite un browser web",
|
||||
"proxyResourcesBannerTitle": "Accesso Pubblico Basato sul Web",
|
||||
"proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili da chiunque tramite Internet da un browser web. A differenza delle risorse private non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.",
|
||||
"proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili a chiunque su Internet tramite un browser web. A differenza delle risorse private, non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.",
|
||||
"clientResourceTitle": "Gestisci Risorse Private",
|
||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||
@@ -174,12 +174,12 @@
|
||||
"authentication": "Autenticazione",
|
||||
"protected": "Protetto",
|
||||
"notProtected": "Non Protetto",
|
||||
"resourceMessageRemove": "Una volta rimossa la risorsa non sarà più accessibile. Tutti gli oggetti target associati alla risorsa saranno rimossi.",
|
||||
"resourceMessageRemove": "Una volta rimossa, la risorsa non sarà più accessibile. Tutti gli obiettivi associati alla risorsa saranno rimossi.",
|
||||
"resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?",
|
||||
"resourceHTTP": "Risorsa HTTPS",
|
||||
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
||||
"resourceRaw": "Risorsa Raw TCP/UDP",
|
||||
"resourceRawDescription": "Richieste proxy su TCP/UDP raw utilizzando un numero di porta.",
|
||||
"resourceRawDescription": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta.",
|
||||
"resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.",
|
||||
"resourceCreate": "Crea Risorsa",
|
||||
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
||||
@@ -192,7 +192,7 @@
|
||||
"selectCountry": "Seleziona paese",
|
||||
"searchCountries": "Cerca paesi...",
|
||||
"noCountryFound": "Nessun paese trovato.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
||||
"resourceType": "Tipo Di Risorsa",
|
||||
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||
@@ -206,13 +206,13 @@
|
||||
"protocol": "Protocollo",
|
||||
"protocolSelect": "Seleziona un protocollo",
|
||||
"resourcePortNumber": "Numero Porta",
|
||||
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste proxy.",
|
||||
"resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"resourceConfig": "Snippet Di Configurazione",
|
||||
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
||||
"resourceAddEntrypoints": "Traefik: Aggiungi Entrypoint",
|
||||
"resourceExposePorts": "Gerbil: espone le porte in Docker Compose",
|
||||
"resourceAddEntrypoints": "Traefik: Aggiungi Ingresso",
|
||||
"resourceExposePorts": "Gerbil: espone le porte in Docker componi",
|
||||
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
||||
"resourceBack": "Torna alle risorse",
|
||||
"resourceGoTo": "Vai alla Risorsa",
|
||||
@@ -228,7 +228,7 @@
|
||||
"rules": "Regole",
|
||||
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
||||
"resourceSetting": "Impostazioni {resourceName}",
|
||||
"alwaysAllow": "Bypass Autenticazione",
|
||||
"alwaysAllow": "Autenticazione Bypass",
|
||||
"alwaysDeny": "Blocca Accesso",
|
||||
"passToAuth": "Passa all'autenticazione",
|
||||
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
||||
@@ -237,11 +237,11 @@
|
||||
"saveGeneralSettings": "Salva Impostazioni Generali",
|
||||
"saveSettings": "Salva Impostazioni",
|
||||
"orgDangerZone": "Zona Pericolosa",
|
||||
"orgDangerZoneDescription": "Una volta che si elimina questa org non sarà possibile tornare indietro, assicurarsi quindi di essere certi della decisione.",
|
||||
"orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.",
|
||||
"orgDelete": "Elimina Organizzazione",
|
||||
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
||||
"orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.",
|
||||
"orgMessageConfirm": "Per confermare digita il nome dell'organizzazione qui sotto.",
|
||||
"orgMessageConfirm": "Per confermare, digita il nome dell'organizzazione qui sotto.",
|
||||
"orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione?",
|
||||
"orgUpdated": "Organizzazione aggiornata",
|
||||
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
||||
@@ -254,10 +254,10 @@
|
||||
"orgDeleted": "Organizzazione eliminata",
|
||||
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
||||
"deleteAccount": "Elimina Account",
|
||||
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.",
|
||||
"deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
||||
"deleteAccountButton": "Elimina Account",
|
||||
"deleteAccountConfirmTitle": "Elimina Account",
|
||||
"deleteAccountConfirmMessage": "Questa operazione cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.",
|
||||
"deleteAccountConfirmMessage": "Questo cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.",
|
||||
"deleteAccountConfirmString": "elimina account",
|
||||
"deleteAccountSuccess": "Account Eliminato",
|
||||
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
||||
@@ -272,7 +272,7 @@
|
||||
"accessUserCreate": "Crea Utente",
|
||||
"accessUserRemove": "Rimuovi Utente",
|
||||
"username": "Nome utente",
|
||||
"identityProvider": "Provider Identità",
|
||||
"identityProvider": "Provider Di Identità",
|
||||
"role": "Ruolo",
|
||||
"nameRequired": "Il nome è obbligatorio",
|
||||
"accessRolesManage": "Gestisci Ruoli",
|
||||
@@ -328,8 +328,8 @@
|
||||
"apiKeysDelete": "Elimina Chiave API",
|
||||
"apiKeysManage": "Gestisci Chiavi API",
|
||||
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
||||
"provisioningKeysTitle": "Chiave di provisioning",
|
||||
"provisioningKeysManage": "Gestisci Chiavi di provisioning",
|
||||
"provisioningKeysTitle": "Chiave Di Provvedimento",
|
||||
"provisioningKeysManage": "Gestisci Chiavi Di Provvedimento",
|
||||
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
||||
"provisioningManage": "Accantonamento",
|
||||
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
||||
@@ -337,25 +337,25 @@
|
||||
"siteApproveSuccess": "Sito approvato con successo",
|
||||
"siteApproveError": "Errore nell'approvazione del sito",
|
||||
"provisioningKeys": "Chiavi Di Provvedimento",
|
||||
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
|
||||
"provisioningKeysAdd": "Genera Chiave di provisioning",
|
||||
"provisioningKeysErrorDelete": "Errore nell'eliminazione della chiave di provisioning",
|
||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave di provisioning",
|
||||
"searchProvisioningKeys": "Cerca i tasti di provisioning ...",
|
||||
"provisioningKeysAdd": "Genera Chiave Di Provvedimento",
|
||||
"provisioningKeysErrorDelete": "Errore nell'eliminare la chiave di provisioning",
|
||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminare la chiave di provisioning",
|
||||
"provisioningKeysQuestionRemove": "Sei sicuro di voler rimuovere questa chiave di provisioning dall'organizzazione?",
|
||||
"provisioningKeysMessageRemove": "Una volta rimossa, la chiave non può più essere utilizzata per il provisioning.",
|
||||
"provisioningKeysDeleteConfirm": "Conferma Eliminazione della chiave di provisioning",
|
||||
"provisioningKeysDeleteConfirm": "Conferma Elimina Chiave Provvisoria",
|
||||
"provisioningKeysDelete": "Elimina chiave di provisioning",
|
||||
"provisioningKeysCreate": "Genera Chiave di provisioning",
|
||||
"provisioningKeysCreate": "Genera Chiave Di Provvedimento",
|
||||
"provisioningKeysCreateDescription": "Genera una nuova chiave di provisioning per l'organizzazione",
|
||||
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
||||
"provisioningKeysSave": "Salva la chiave di provisioning",
|
||||
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
||||
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
||||
"provisioningKeysList": "Nuova chiave di provisioning",
|
||||
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
|
||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
|
||||
"provisioningKeysMaxBatchSize": "Dimensione massima lotto",
|
||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del lotto (nessun limite)",
|
||||
"provisioningKeysMaxBatchUnlimited": "Illimitato",
|
||||
"provisioningKeysMaxBatchSizeInvalid": "Inserisci una dimensione massima valida del batch (1–1.000.000).",
|
||||
"provisioningKeysMaxBatchSizeInvalid": "Inserisci un lotto massimo valido (1–1.000.000).",
|
||||
"provisioningKeysValidUntil": "Valido fino al",
|
||||
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
||||
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
||||
@@ -363,14 +363,14 @@
|
||||
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
||||
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
||||
"provisioningKeysNeverUsed": "Mai",
|
||||
"provisioningKeysEdit": "Modifica Chiave di provisioning",
|
||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
|
||||
"provisioningKeysEdit": "Modifica Chiave Di Provvedimento",
|
||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del lotto e il tempo di scadenza per questa chiave.",
|
||||
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
||||
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
||||
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
|
||||
"provisioningKeysUpdated": "Chiave di provisioning aggiornata",
|
||||
"provisioningKeysUpdated": "Chiave di accantonamento aggiornata",
|
||||
"provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.",
|
||||
"provisioningKeysBannerTitle": "Chiavi di provisioning del Sito",
|
||||
"provisioningKeysBannerTitle": "Chiavi Di Provvedimento Sito",
|
||||
"provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.",
|
||||
"provisioningKeysBannerButtonText": "Scopri di più",
|
||||
"pendingSitesBannerTitle": "Siti In Attesa",
|
||||
@@ -386,7 +386,7 @@
|
||||
"userErrorDelete": "Errore nell'eliminare l'utente",
|
||||
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
||||
"userDeleteServer": "Elimina utente dal server",
|
||||
"userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni e verrà completamente rimosso dal server.",
|
||||
"userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni ed essere completamente rimosso dal server.",
|
||||
"userQuestionRemove": "Sei sicuro di voler eliminare definitivamente l'utente dal server?",
|
||||
"licenseKey": "Chiave Di Licenza",
|
||||
"valid": "Valido",
|
||||
@@ -404,9 +404,9 @@
|
||||
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
||||
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
||||
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
||||
"licenseAbout": "Informazioni sul Licensing",
|
||||
"licenseAbout": "Informazioni Su Licenze",
|
||||
"communityEdition": "Edizione Community",
|
||||
"licenseAboutDescription": "Questa sezione è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.",
|
||||
"licenseAboutDescription": "Questo è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.",
|
||||
"licenseKeyActivated": "Chiave di licenza attivata",
|
||||
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
||||
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
||||
@@ -429,7 +429,7 @@
|
||||
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
||||
"licensedNot": "Non Licenziato",
|
||||
"hostId": "ID Host",
|
||||
"licenseReckeckAll": "Ricontrolla Tutte le chiavi",
|
||||
"licenseReckeckAll": "Ricontrolla Tutte Le Tasti",
|
||||
"licenseSiteUsage": "Utilizzo Siti",
|
||||
"licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.",
|
||||
"licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.",
|
||||
@@ -480,7 +480,7 @@
|
||||
"userOrgRemoved": "Utente rimosso",
|
||||
"userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.",
|
||||
"userQuestionOrgRemove": "Sei sicuro di voler rimuovere questo utente dall'organizzazione?",
|
||||
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||
"userMessageOrgRemove": "Una volta rimosso, questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||
"users": "Utenti",
|
||||
@@ -532,13 +532,13 @@
|
||||
"approve": "Approva",
|
||||
"approved": "Approvato",
|
||||
"denied": "Negato",
|
||||
"deniedApproval": "Approvazione Negata",
|
||||
"deniedApproval": "Omologazione Negata",
|
||||
"all": "Tutti",
|
||||
"deny": "Nega",
|
||||
"viewDetails": "Visualizza Dettagli",
|
||||
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
||||
"resetFilters": "Ripristina Filtri",
|
||||
"totalBlocked": "Richieste Bloccate Da Pangolin",
|
||||
"totalBlocked": "Richieste Bloccate Da Pangolino",
|
||||
"totalRequests": "Totale Richieste",
|
||||
"requestsByCountry": "Richieste Per Paese",
|
||||
"requestsByDay": "Richieste Per Giorno",
|
||||
@@ -546,7 +546,7 @@
|
||||
"allowed": "Consentito",
|
||||
"topCountries": "Paesi Principali",
|
||||
"accessRoleSelect": "Seleziona ruolo",
|
||||
"inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. L'utente deve accedere al link per accettare l'invito.",
|
||||
"inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.",
|
||||
"inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.",
|
||||
"inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
|
||||
"idpTitle": "Informazioni Generali",
|
||||
@@ -562,7 +562,7 @@
|
||||
"userSaved": "Utente salvato",
|
||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||
"autoProvisioned": "Auto Provisioned",
|
||||
"autoProvisionSettings": "Impostazioni Automatiche di provisioning",
|
||||
"autoProvisionSettings": "Impostazioni Automatiche Di Fornitura",
|
||||
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||
"accessControlsSubmit": "Salva Controlli di Accesso",
|
||||
@@ -576,9 +576,9 @@
|
||||
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
|
||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||
"proxyEnableSSL": "Abilita SSL",
|
||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.",
|
||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure agli obiettivi.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configura Risorse Interne",
|
||||
"configureTarget": "Configura Obiettivi",
|
||||
"targetErrorFetch": "Impossibile recuperare i target",
|
||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||
|
||||
@@ -479,7 +479,10 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -607,7 +610,10 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return targets
|
||||
|
||||
@@ -671,7 +671,10 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -799,7 +802,10 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
);
|
||||
|
||||
return targets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@server/db";
|
||||
import { sites, clients, olms } from "@server/db";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ import logger from "@server/logger";
|
||||
*/
|
||||
|
||||
const FLUSH_INTERVAL_MS = 10_000; // Flush every 10 seconds
|
||||
const MAX_RETRIES = 5;
|
||||
const MAX_RETRIES = 2;
|
||||
const BASE_DELAY_MS = 50;
|
||||
|
||||
// ── Site (newt) pings ──────────────────────────────────────────────────
|
||||
@@ -36,14 +36,6 @@ const pendingOlmArchiveResets: Set<string> = new Set();
|
||||
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Guard that prevents two flush cycles from running concurrently.
|
||||
* setInterval does not await async callbacks, so without this a slow flush
|
||||
* (e.g. due to DB latency) would overlap with the next scheduled cycle and
|
||||
* the two concurrent bulk UPDATEs would deadlock each other.
|
||||
*/
|
||||
let isFlushing = false;
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -80,12 +72,6 @@ export function recordClientPing(
|
||||
|
||||
/**
|
||||
* Flush all accumulated site pings to the database.
|
||||
*
|
||||
* Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE
|
||||
* statement. We use the maximum timestamp across the batch so that `lastPing`
|
||||
* reflects the most recent ping seen for any site in the group. This avoids
|
||||
* the multi-statement transaction that previously created additional
|
||||
* row-lock ordering hazards.
|
||||
*/
|
||||
async function flushSitePingsToDb(): Promise<void> {
|
||||
if (pendingSitePings.size === 0) {
|
||||
@@ -97,35 +83,55 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
const pingsToFlush = new Map(pendingSitePings);
|
||||
pendingSitePings.clear();
|
||||
|
||||
const entries = Array.from(pingsToFlush.entries());
|
||||
// Sort by siteId for consistent lock ordering (prevents deadlocks)
|
||||
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
|
||||
([a], [b]) => a - b
|
||||
);
|
||||
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
// Use the latest timestamp in the batch so that `lastPing` always
|
||||
// moves forward. Using a single timestamp for the whole batch means
|
||||
// we only ever need one UPDATE statement (no transaction).
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const siteIds = batch.map(([id]) => id);
|
||||
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
|
||||
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
await db
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: maxTimestamp
|
||||
})
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
// Group by timestamp for efficient bulk updates
|
||||
const byTimestamp = new Map<number, number[]>();
|
||||
for (const [siteId, timestamp] of batch) {
|
||||
const group = byTimestamp.get(timestamp) || [];
|
||||
group.push(siteId);
|
||||
byTimestamp.set(timestamp, group);
|
||||
}
|
||||
|
||||
if (byTimestamp.size === 1) {
|
||||
const [timestamp, siteIds] = Array.from(
|
||||
byTimestamp.entries()
|
||||
)[0];
|
||||
await db
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: timestamp
|
||||
})
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
} else {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [timestamp, siteIds] of byTimestamp) {
|
||||
await tx
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: timestamp
|
||||
})
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "flushSitePingsToDb");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`,
|
||||
{ error }
|
||||
);
|
||||
// Re-queue only if the preserved timestamp is newer than any
|
||||
// update that may have landed since we snapshotted.
|
||||
for (const [siteId, timestamp] of batch) {
|
||||
const existing = pendingSitePings.get(siteId);
|
||||
if (!existing || existing < timestamp) {
|
||||
@@ -138,8 +144,6 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Flush all accumulated client (OLM) pings to the database.
|
||||
*
|
||||
* Same single-UPDATE-per-batch approach as `flushSitePingsToDb`.
|
||||
*/
|
||||
async function flushClientPingsToDb(): Promise<void> {
|
||||
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
|
||||
@@ -155,25 +159,51 @@ async function flushClientPingsToDb(): Promise<void> {
|
||||
|
||||
// ── Flush client pings ─────────────────────────────────────────────
|
||||
if (pingsToFlush.size > 0) {
|
||||
const entries = Array.from(pingsToFlush.entries());
|
||||
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
|
||||
([a], [b]) => a - b
|
||||
);
|
||||
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const clientIds = batch.map(([id]) => id);
|
||||
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
|
||||
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
await db
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: maxTimestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.where(inArray(clients.clientId, clientIds));
|
||||
const byTimestamp = new Map<number, number[]>();
|
||||
for (const [clientId, timestamp] of batch) {
|
||||
const group = byTimestamp.get(timestamp) || [];
|
||||
group.push(clientId);
|
||||
byTimestamp.set(timestamp, group);
|
||||
}
|
||||
|
||||
if (byTimestamp.size === 1) {
|
||||
const [timestamp, clientIds] = Array.from(
|
||||
byTimestamp.entries()
|
||||
)[0];
|
||||
await db
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: timestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.where(inArray(clients.clientId, clientIds));
|
||||
} else {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [timestamp, clientIds] of byTimestamp) {
|
||||
await tx
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: timestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.where(
|
||||
inArray(clients.clientId, clientIds)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "flushClientPingsToDb");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -230,12 +260,7 @@ export async function flushPingsToDb(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Simple retry wrapper with exponential backoff for transient errors
|
||||
* (deadlocks, connection timeouts, unexpected disconnects).
|
||||
*
|
||||
* PostgreSQL deadlocks (40P01) are always safe to retry: the database
|
||||
* guarantees exactly one winner per deadlock pair, so the loser just needs
|
||||
* to try again. MAX_RETRIES is intentionally higher than typical connection
|
||||
* retry budgets to give deadlock victims enough chances to succeed.
|
||||
* (connection timeouts, unexpected disconnects).
|
||||
*/
|
||||
async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
@@ -252,8 +277,7 @@ async function withRetry<T>(
|
||||
const jitter = Math.random() * baseDelay;
|
||||
const delay = baseDelay + jitter;
|
||||
logger.warn(
|
||||
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`,
|
||||
{ code: error?.code ?? error?.cause?.code }
|
||||
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
@@ -264,14 +288,14 @@ async function withRetry<T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect transient errors that are safe to retry.
|
||||
* Detect transient connection errors that are safe to retry.
|
||||
*/
|
||||
function isTransientError(error: any): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
const message = (error.message || "").toLowerCase();
|
||||
const causeMessage = (error.cause?.message || "").toLowerCase();
|
||||
const code = error.code || error.cause?.code || "";
|
||||
const code = error.code || "";
|
||||
|
||||
// Connection timeout / terminated
|
||||
if (
|
||||
@@ -284,17 +308,12 @@ function isTransientError(error: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
|
||||
// PostgreSQL deadlock
|
||||
if (code === "40P01" || message.includes("deadlock")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PostgreSQL serialization failure
|
||||
if (code === "40001") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
|
||||
// ECONNRESET, ECONNREFUSED, EPIPE
|
||||
if (
|
||||
code === "ECONNRESET" ||
|
||||
code === "ECONNREFUSED" ||
|
||||
@@ -318,26 +337,12 @@ export function startPingAccumulator(): void {
|
||||
}
|
||||
|
||||
flushTimer = setInterval(async () => {
|
||||
// Skip this tick if the previous flush is still in progress.
|
||||
// setInterval does not await async callbacks, so without this guard
|
||||
// two flush cycles can run concurrently and deadlock each other on
|
||||
// overlapping bulk UPDATE statements.
|
||||
if (isFlushing) {
|
||||
logger.debug(
|
||||
"Ping accumulator: previous flush still in progress, skipping cycle"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isFlushing = true;
|
||||
try {
|
||||
await flushPingsToDb();
|
||||
} catch (error) {
|
||||
logger.error("Unhandled error in ping accumulator flush", {
|
||||
error
|
||||
});
|
||||
} finally {
|
||||
isFlushing = false;
|
||||
}
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
|
||||
@@ -359,22 +364,7 @@ export async function stopPingAccumulator(): Promise<void> {
|
||||
flushTimer = null;
|
||||
}
|
||||
|
||||
// Final flush to persist any remaining pings.
|
||||
// Wait for any in-progress flush to finish first so we don't race.
|
||||
if (isFlushing) {
|
||||
logger.debug(
|
||||
"Ping accumulator: waiting for in-progress flush before stopping…"
|
||||
);
|
||||
await new Promise<void>((resolve) => {
|
||||
const poll = setInterval(() => {
|
||||
if (!isFlushing) {
|
||||
clearInterval(poll);
|
||||
resolve();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
// Final flush to persist any remaining pings
|
||||
try {
|
||||
await flushPingsToDb();
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user