mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-11 12:26:35 +00:00
Compare commits
1 Commits
crowdin_de
...
breakout-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48abb9e98c |
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Безплатен предоставен домейн",
|
"domainPickerFreeProvidedDomain": "Безплатен предоставен домейн",
|
||||||
"domainPickerVerified": "Проверено",
|
"domainPickerVerified": "Проверено",
|
||||||
"domainPickerUnverified": "Непроверено",
|
"domainPickerUnverified": "Непроверено",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
|
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
|
||||||
"domainPickerError": "Грешка",
|
"domainPickerError": "Грешка",
|
||||||
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
|
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména",
|
"domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména",
|
||||||
"domainPickerVerified": "Ověřeno",
|
"domainPickerVerified": "Ověřeno",
|
||||||
"domainPickerUnverified": "Neověřeno",
|
"domainPickerUnverified": "Neověřeno",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.",
|
"domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.",
|
||||||
"domainPickerError": "Chyba",
|
"domainPickerError": "Chyba",
|
||||||
"domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace",
|
"domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
||||||
"domainPickerVerified": "Verifiziert",
|
"domainPickerVerified": "Verifiziert",
|
||||||
"domainPickerUnverified": "Nicht verifiziert",
|
"domainPickerUnverified": "Nicht verifiziert",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
||||||
"domainPickerError": "Fehler",
|
"domainPickerError": "Fehler",
|
||||||
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains",
|
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||||
"domainPickerVerified": "Verificado",
|
"domainPickerVerified": "Verificado",
|
||||||
"domainPickerUnverified": "Sin verificar",
|
"domainPickerUnverified": "Sin verificar",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
||||||
"domainPickerError": "Error",
|
"domainPickerError": "Error",
|
||||||
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||||
"domainPickerVerified": "Vérifié",
|
"domainPickerVerified": "Vérifié",
|
||||||
"domainPickerUnverified": "Non vérifié",
|
"domainPickerUnverified": "Non vérifié",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.",
|
"domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.",
|
||||||
"domainPickerError": "Erreur",
|
"domainPickerError": "Erreur",
|
||||||
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Creare l'organizzazione, il sito e le risorse",
|
"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",
|
"headerAuthCompatibility": "Compatibilità estesa",
|
||||||
"setupNewOrg": "Nuova Organizzazione",
|
"setupNewOrg": "Nuova Organizzazione",
|
||||||
"setupCreateOrg": "Crea Organizzazione",
|
"setupCreateOrg": "Crea Organizzazione",
|
||||||
"setupCreateResources": "Crea Risorse",
|
"setupCreateResources": "Crea Risorse",
|
||||||
"setupOrgName": "Nome dell'Organizzazione",
|
"setupOrgName": "Nome Dell'Organizzazione",
|
||||||
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
"orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.",
|
||||||
"orgId": "Id Organizzazione",
|
"orgId": "Id Organizzazione",
|
||||||
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
"setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.",
|
||||||
"setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.",
|
"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.",
|
"componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.",
|
||||||
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
|
||||||
"welcome": "Benvenuto su Pangolin!",
|
"welcome": "Benvenuti a Pangolin",
|
||||||
"welcomeTo": "Benvenuto su Pangolin!",
|
"welcomeTo": "Benvenuto a",
|
||||||
"componentsCreateOrg": "Crea un'organizzazione",
|
"componentsCreateOrg": "Crea un'organizzazione",
|
||||||
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
|
"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à.",
|
"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.",
|
"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.",
|
"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.",
|
"inviteCreateUser": "Si prega di creare un account prima.",
|
||||||
"goHome": "Vai alla Home",
|
"goHome": "Vai A Home",
|
||||||
"inviteLogInOtherUser": "Accedi come utente diverso",
|
"inviteLogInOtherUser": "Accedi come utente diverso",
|
||||||
"createAnAccount": "Crea un account",
|
"createAnAccount": "Crea un account",
|
||||||
"inviteNotAccepted": "Invito Non Accettato",
|
"inviteNotAccepted": "Invito Non Accettato",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
||||||
"siteDelete": "Elimina 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?",
|
"siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
|
||||||
"siteManageSites": "Gestisci Siti",
|
"siteManageSites": "Gestisci Siti",
|
||||||
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
"siteLoadWGConfig": "Caricamento configurazione WireGuard...",
|
||||||
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
"siteDocker": "Espandi per i dettagli di distribuzione Docker",
|
||||||
"toggle": "Attiva/disattiva",
|
"toggle": "Attiva/disattiva",
|
||||||
"dockerCompose": "Docker Compose",
|
"dockerCompose": "Composizione Docker",
|
||||||
"dockerRun": "Corsa 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",
|
"siteConfirmCopy": "Ho copiato la configurazione",
|
||||||
"searchSitesProgress": "Cerca siti...",
|
"searchSitesProgress": "Cerca siti...",
|
||||||
"siteAdd": "Aggiungi Sito",
|
"siteAdd": "Aggiungi Sito",
|
||||||
@@ -88,29 +88,29 @@
|
|||||||
"operatingSystem": "Sistema Operativo",
|
"operatingSystem": "Sistema Operativo",
|
||||||
"commands": "Comandi",
|
"commands": "Comandi",
|
||||||
"recommended": "Consigliato",
|
"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",
|
"siteRunsInDocker": "Esegue nel Docker",
|
||||||
"siteRunsInShell": "Esegue in shell su macOS, Linux e Windows",
|
"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",
|
"siteErrorUpdate": "Impossibile aggiornare il sito",
|
||||||
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
"siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.",
|
||||||
"siteUpdated": "Sito aggiornato",
|
"siteUpdated": "Sito aggiornato",
|
||||||
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
"siteUpdatedDescription": "Il sito è stato aggiornato.",
|
||||||
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
|
||||||
"siteSettingDescription": "Configura le impostazioni del sito",
|
"siteSettingDescription": "Configura le impostazioni del sito",
|
||||||
"siteSetting": "Impostazioni del sito {siteName}",
|
"siteSetting": "Impostazioni {siteName}",
|
||||||
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
||||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
||||||
"siteWg": "WireGuard Base",
|
"siteWg": "WireGuard Base",
|
||||||
"siteWgDescription": "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 un 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.",
|
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.",
|
||||||
"siteSeeAll": "Vedi Tutti I Siti",
|
"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",
|
"siteNewtCredentials": "Credenziali",
|
||||||
"siteNewtCredentialsDescription": "Questo è come il sito si autenticherà con il server",
|
"siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server",
|
||||||
"remoteNodeCredentialsDescription": "Questo è il modo in cui il nodo remoto si autenticherà con il server",
|
"remoteNodeCredentialsDescription": "Questo è come il nodo remoto si autenticherà con il server",
|
||||||
"siteCredentialsSave": "Salva le credenziali",
|
"siteCredentialsSave": "Salva le credenziali",
|
||||||
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
"siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
|
||||||
"siteInfo": "Informazioni Sito",
|
"siteInfo": "Informazioni Sito",
|
||||||
@@ -140,8 +140,8 @@
|
|||||||
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
"shareCreateDescription": "Chiunque con questo link può accedere alla risorsa",
|
||||||
"shareTitleOptional": "Titolo (facoltativo)",
|
"shareTitleOptional": "Titolo (facoltativo)",
|
||||||
"expireIn": "Scadenza In",
|
"expireIn": "Scadenza In",
|
||||||
"neverExpire": "Nessuna scadenza",
|
"neverExpire": "Mai scadere",
|
||||||
"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.",
|
"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.",
|
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
|
||||||
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
|
||||||
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
|
||||||
@@ -161,9 +161,9 @@
|
|||||||
"never": "Mai",
|
"never": "Mai",
|
||||||
"shareErrorSelectResource": "Seleziona una risorsa",
|
"shareErrorSelectResource": "Seleziona una risorsa",
|
||||||
"proxyResourceTitle": "Gestisci Risorse Pubbliche",
|
"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",
|
"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",
|
"clientResourceTitle": "Gestisci Risorse Private",
|
||||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||||
@@ -174,12 +174,12 @@
|
|||||||
"authentication": "Autenticazione",
|
"authentication": "Autenticazione",
|
||||||
"protected": "Protetto",
|
"protected": "Protetto",
|
||||||
"notProtected": "Non 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?",
|
"resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?",
|
||||||
"resourceHTTP": "Risorsa HTTPS",
|
"resourceHTTP": "Risorsa HTTPS",
|
||||||
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
"resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.",
|
||||||
"resourceRaw": "Risorsa Raw TCP/UDP",
|
"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.",
|
"resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.",
|
||||||
"resourceCreate": "Crea Risorsa",
|
"resourceCreate": "Crea Risorsa",
|
||||||
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
"resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa",
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
"selectCountry": "Seleziona paese",
|
"selectCountry": "Seleziona paese",
|
||||||
"searchCountries": "Cerca paesi...",
|
"searchCountries": "Cerca paesi...",
|
||||||
"noCountryFound": "Nessun paese trovato.",
|
"noCountryFound": "Nessun paese trovato.",
|
||||||
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
||||||
"resourceType": "Tipo Di Risorsa",
|
"resourceType": "Tipo Di Risorsa",
|
||||||
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
"resourceTypeDescription": "Determinare come accedere alla risorsa",
|
||||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||||
@@ -206,13 +206,13 @@
|
|||||||
"protocol": "Protocollo",
|
"protocol": "Protocollo",
|
||||||
"protocolSelect": "Seleziona un protocollo",
|
"protocolSelect": "Seleziona un protocollo",
|
||||||
"resourcePortNumber": "Numero Porta",
|
"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",
|
"back": "Indietro",
|
||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"resourceConfig": "Snippet Di Configurazione",
|
"resourceConfig": "Snippet Di Configurazione",
|
||||||
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
"resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP",
|
||||||
"resourceAddEntrypoints": "Traefik: Aggiungi Entrypoint",
|
"resourceAddEntrypoints": "Traefik: Aggiungi Ingresso",
|
||||||
"resourceExposePorts": "Gerbil: espone le porte in Docker Compose",
|
"resourceExposePorts": "Gerbil: espone le porte in Docker componi",
|
||||||
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
"resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP",
|
||||||
"resourceBack": "Torna alle risorse",
|
"resourceBack": "Torna alle risorse",
|
||||||
"resourceGoTo": "Vai alla Risorsa",
|
"resourceGoTo": "Vai alla Risorsa",
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
"rules": "Regole",
|
"rules": "Regole",
|
||||||
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
"resourceSettingDescription": "Configura le impostazioni sulla risorsa",
|
||||||
"resourceSetting": "Impostazioni {resourceName}",
|
"resourceSetting": "Impostazioni {resourceName}",
|
||||||
"alwaysAllow": "Bypass Autenticazione",
|
"alwaysAllow": "Autenticazione Bypass",
|
||||||
"alwaysDeny": "Blocca Accesso",
|
"alwaysDeny": "Blocca Accesso",
|
||||||
"passToAuth": "Passa all'autenticazione",
|
"passToAuth": "Passa all'autenticazione",
|
||||||
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
"orgSettingsDescription": "Configura le impostazioni dell'organizzazione",
|
||||||
@@ -237,11 +237,11 @@
|
|||||||
"saveGeneralSettings": "Salva Impostazioni Generali",
|
"saveGeneralSettings": "Salva Impostazioni Generali",
|
||||||
"saveSettings": "Salva Impostazioni",
|
"saveSettings": "Salva Impostazioni",
|
||||||
"orgDangerZone": "Zona Pericolosa",
|
"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",
|
"orgDelete": "Elimina Organizzazione",
|
||||||
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
"orgDeleteConfirm": "Conferma Elimina Organizzazione",
|
||||||
"orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.",
|
"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?",
|
"orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione?",
|
||||||
"orgUpdated": "Organizzazione aggiornata",
|
"orgUpdated": "Organizzazione aggiornata",
|
||||||
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
"orgUpdatedDescription": "L'organizzazione è stata aggiornata.",
|
||||||
@@ -254,10 +254,10 @@
|
|||||||
"orgDeleted": "Organizzazione eliminata",
|
"orgDeleted": "Organizzazione eliminata",
|
||||||
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
"orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.",
|
||||||
"deleteAccount": "Elimina Account",
|
"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",
|
"deleteAccountButton": "Elimina Account",
|
||||||
"deleteAccountConfirmTitle": "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",
|
"deleteAccountConfirmString": "elimina account",
|
||||||
"deleteAccountSuccess": "Account Eliminato",
|
"deleteAccountSuccess": "Account Eliminato",
|
||||||
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
"deleteAccountSuccessMessage": "Il tuo account è stato eliminato.",
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
"accessUserCreate": "Crea Utente",
|
"accessUserCreate": "Crea Utente",
|
||||||
"accessUserRemove": "Rimuovi Utente",
|
"accessUserRemove": "Rimuovi Utente",
|
||||||
"username": "Nome utente",
|
"username": "Nome utente",
|
||||||
"identityProvider": "Provider Identità",
|
"identityProvider": "Provider Di Identità",
|
||||||
"role": "Ruolo",
|
"role": "Ruolo",
|
||||||
"nameRequired": "Il nome è obbligatorio",
|
"nameRequired": "Il nome è obbligatorio",
|
||||||
"accessRolesManage": "Gestisci Ruoli",
|
"accessRolesManage": "Gestisci Ruoli",
|
||||||
@@ -328,8 +328,8 @@
|
|||||||
"apiKeysDelete": "Elimina Chiave API",
|
"apiKeysDelete": "Elimina Chiave API",
|
||||||
"apiKeysManage": "Gestisci Chiavi API",
|
"apiKeysManage": "Gestisci Chiavi API",
|
||||||
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
||||||
"provisioningKeysTitle": "Chiave di provisioning",
|
"provisioningKeysTitle": "Chiave Di Provvedimento",
|
||||||
"provisioningKeysManage": "Gestisci Chiavi di provisioning",
|
"provisioningKeysManage": "Gestisci Chiavi Di Provvedimento",
|
||||||
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
||||||
"provisioningManage": "Accantonamento",
|
"provisioningManage": "Accantonamento",
|
||||||
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
|
||||||
@@ -337,25 +337,25 @@
|
|||||||
"siteApproveSuccess": "Sito approvato con successo",
|
"siteApproveSuccess": "Sito approvato con successo",
|
||||||
"siteApproveError": "Errore nell'approvazione del sito",
|
"siteApproveError": "Errore nell'approvazione del sito",
|
||||||
"provisioningKeys": "Chiavi Di Provvedimento",
|
"provisioningKeys": "Chiavi Di Provvedimento",
|
||||||
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
|
"searchProvisioningKeys": "Cerca i tasti di provisioning ...",
|
||||||
"provisioningKeysAdd": "Genera Chiave di provisioning",
|
"provisioningKeysAdd": "Genera Chiave Di Provvedimento",
|
||||||
"provisioningKeysErrorDelete": "Errore nell'eliminazione della chiave di provisioning",
|
"provisioningKeysErrorDelete": "Errore nell'eliminare la chiave di provisioning",
|
||||||
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave di provisioning",
|
"provisioningKeysErrorDeleteMessage": "Errore nell'eliminare la chiave di provisioning",
|
||||||
"provisioningKeysQuestionRemove": "Sei sicuro di voler rimuovere questa chiave di provisioning dall'organizzazione?",
|
"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.",
|
"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",
|
"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",
|
"provisioningKeysCreateDescription": "Genera una nuova chiave di provisioning per l'organizzazione",
|
||||||
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
"provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning",
|
||||||
"provisioningKeysSave": "Salva la chiave di provisioning",
|
"provisioningKeysSave": "Salva la chiave di provisioning",
|
||||||
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
|
||||||
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
|
||||||
"provisioningKeysList": "Nuova chiave di provisioning",
|
"provisioningKeysList": "Nuova chiave di provisioning",
|
||||||
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
|
"provisioningKeysMaxBatchSize": "Dimensione massima lotto",
|
||||||
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
|
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del lotto (nessun limite)",
|
||||||
"provisioningKeysMaxBatchUnlimited": "Illimitato",
|
"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",
|
"provisioningKeysValidUntil": "Valido fino al",
|
||||||
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
|
||||||
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
|
||||||
@@ -363,14 +363,14 @@
|
|||||||
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
"provisioningKeysLastUsed": "Ultimo utilizzo",
|
||||||
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
"provisioningKeysNoExpiry": "Nessuna scadenza",
|
||||||
"provisioningKeysNeverUsed": "Mai",
|
"provisioningKeysNeverUsed": "Mai",
|
||||||
"provisioningKeysEdit": "Modifica Chiave di provisioning",
|
"provisioningKeysEdit": "Modifica Chiave Di Provvedimento",
|
||||||
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
|
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del lotto e il tempo di scadenza per questa chiave.",
|
||||||
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
"provisioningKeysApproveNewSites": "Approva nuovi siti",
|
||||||
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
|
||||||
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
|
"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.",
|
"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.",
|
"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ù",
|
"provisioningKeysBannerButtonText": "Scopri di più",
|
||||||
"pendingSitesBannerTitle": "Siti In Attesa",
|
"pendingSitesBannerTitle": "Siti In Attesa",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
"userErrorDelete": "Errore nell'eliminare l'utente",
|
"userErrorDelete": "Errore nell'eliminare l'utente",
|
||||||
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
"userDeleteConfirm": "Conferma Eliminazione Utente",
|
||||||
"userDeleteServer": "Elimina utente dal server",
|
"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?",
|
"userQuestionRemove": "Sei sicuro di voler eliminare definitivamente l'utente dal server?",
|
||||||
"licenseKey": "Chiave Di Licenza",
|
"licenseKey": "Chiave Di Licenza",
|
||||||
"valid": "Valido",
|
"valid": "Valido",
|
||||||
@@ -404,9 +404,9 @@
|
|||||||
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
"licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.",
|
||||||
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
"licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita",
|
||||||
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
"licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.",
|
||||||
"licenseAbout": "Informazioni sul Licensing",
|
"licenseAbout": "Informazioni Su Licenze",
|
||||||
"communityEdition": "Edizione Community",
|
"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",
|
"licenseKeyActivated": "Chiave di licenza attivata",
|
||||||
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
"licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.",
|
||||||
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
"licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza",
|
||||||
@@ -429,7 +429,7 @@
|
|||||||
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
|
||||||
"licensedNot": "Non Licenziato",
|
"licensedNot": "Non Licenziato",
|
||||||
"hostId": "ID Host",
|
"hostId": "ID Host",
|
||||||
"licenseReckeckAll": "Ricontrolla Tutte le chiavi",
|
"licenseReckeckAll": "Ricontrolla Tutte Le Tasti",
|
||||||
"licenseSiteUsage": "Utilizzo Siti",
|
"licenseSiteUsage": "Utilizzo Siti",
|
||||||
"licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.",
|
"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.",
|
"licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.",
|
||||||
@@ -480,7 +480,7 @@
|
|||||||
"userOrgRemoved": "Utente rimosso",
|
"userOrgRemoved": "Utente rimosso",
|
||||||
"userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.",
|
"userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.",
|
||||||
"userQuestionOrgRemove": "Sei sicuro di voler rimuovere questo utente 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",
|
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
@@ -532,13 +532,13 @@
|
|||||||
"approve": "Approva",
|
"approve": "Approva",
|
||||||
"approved": "Approvato",
|
"approved": "Approvato",
|
||||||
"denied": "Negato",
|
"denied": "Negato",
|
||||||
"deniedApproval": "Approvazione Negata",
|
"deniedApproval": "Omologazione Negata",
|
||||||
"all": "Tutti",
|
"all": "Tutti",
|
||||||
"deny": "Nega",
|
"deny": "Nega",
|
||||||
"viewDetails": "Visualizza Dettagli",
|
"viewDetails": "Visualizza Dettagli",
|
||||||
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
"requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo",
|
||||||
"resetFilters": "Ripristina Filtri",
|
"resetFilters": "Ripristina Filtri",
|
||||||
"totalBlocked": "Richieste Bloccate Da Pangolin",
|
"totalBlocked": "Richieste Bloccate Da Pangolino",
|
||||||
"totalRequests": "Totale Richieste",
|
"totalRequests": "Totale Richieste",
|
||||||
"requestsByCountry": "Richieste Per Paese",
|
"requestsByCountry": "Richieste Per Paese",
|
||||||
"requestsByDay": "Richieste Per Giorno",
|
"requestsByDay": "Richieste Per Giorno",
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
"allowed": "Consentito",
|
"allowed": "Consentito",
|
||||||
"topCountries": "Paesi Principali",
|
"topCountries": "Paesi Principali",
|
||||||
"accessRoleSelect": "Seleziona ruolo",
|
"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.",
|
"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}}.",
|
"inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
|
||||||
"idpTitle": "Informazioni Generali",
|
"idpTitle": "Informazioni Generali",
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
"userSaved": "Utente salvato",
|
"userSaved": "Utente salvato",
|
||||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||||
"autoProvisioned": "Auto Provisioned",
|
"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à",
|
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
||||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||||
"accessControlsSubmit": "Salva Controlli di Accesso",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||||
"proxyEnableSSL": "Abilita SSL",
|
"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",
|
"target": "Target",
|
||||||
"configureTarget": "Configura Risorse Interne",
|
"configureTarget": "Configura Obiettivi",
|
||||||
"targetErrorFetch": "Impossibile recuperare i target",
|
"targetErrorFetch": "Impossibile recuperare i target",
|
||||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||||
"domainPickerVerified": "Verificato",
|
"domainPickerVerified": "Verificato",
|
||||||
"domainPickerUnverified": "Non Verificato",
|
"domainPickerUnverified": "Non Verificato",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
||||||
"domainPickerError": "Errore",
|
"domainPickerError": "Errore",
|
||||||
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||||
"domainPickerVerified": "검증됨",
|
"domainPickerVerified": "검증됨",
|
||||||
"domainPickerUnverified": "검증되지 않음",
|
"domainPickerUnverified": "검증되지 않음",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
||||||
"domainPickerError": "오류",
|
"domainPickerError": "오류",
|
||||||
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
||||||
"domainPickerVerified": "Bekreftet",
|
"domainPickerVerified": "Bekreftet",
|
||||||
"domainPickerUnverified": "Uverifisert",
|
"domainPickerUnverified": "Uverifisert",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
||||||
"domainPickerError": "Feil",
|
"domainPickerError": "Feil",
|
||||||
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||||
"domainPickerVerified": "Geverifieerd",
|
"domainPickerVerified": "Geverifieerd",
|
||||||
"domainPickerUnverified": "Ongeverifieerd",
|
"domainPickerUnverified": "Ongeverifieerd",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
||||||
"domainPickerError": "Foutmelding",
|
"domainPickerError": "Foutmelding",
|
||||||
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||||
"domainPickerVerified": "Zweryfikowano",
|
"domainPickerVerified": "Zweryfikowano",
|
||||||
"domainPickerUnverified": "Niezweryfikowane",
|
"domainPickerUnverified": "Niezweryfikowane",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
||||||
"domainPickerError": "Błąd",
|
"domainPickerError": "Błąd",
|
||||||
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||||
"domainPickerVerified": "Verificada",
|
"domainPickerVerified": "Verificada",
|
||||||
"domainPickerUnverified": "Não verificado",
|
"domainPickerUnverified": "Não verificado",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
||||||
"domainPickerError": "ERRO",
|
"domainPickerError": "ERRO",
|
||||||
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||||
"domainPickerVerified": "Подтверждено",
|
"domainPickerVerified": "Подтверждено",
|
||||||
"domainPickerUnverified": "Не подтверждено",
|
"domainPickerUnverified": "Не подтверждено",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
||||||
"domainPickerError": "Ошибка",
|
"domainPickerError": "Ошибка",
|
||||||
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
||||||
"domainPickerVerified": "Doğrulandı",
|
"domainPickerVerified": "Doğrulandı",
|
||||||
"domainPickerUnverified": "Doğrulanmadı",
|
"domainPickerUnverified": "Doğrulanmadı",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
||||||
"domainPickerError": "Hata",
|
"domainPickerError": "Hata",
|
||||||
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
||||||
|
|||||||
@@ -2116,7 +2116,6 @@
|
|||||||
"domainPickerFreeProvidedDomain": "免费提供的域",
|
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||||
"domainPickerVerified": "已验证",
|
"domainPickerVerified": "已验证",
|
||||||
"domainPickerUnverified": "未验证",
|
"domainPickerUnverified": "未验证",
|
||||||
"domainPickerManual": "Manual",
|
|
||||||
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
||||||
"domainPickerError": "错误",
|
"domainPickerError": "错误",
|
||||||
"domainPickerErrorLoadDomains": "加载组织域名失败",
|
"domainPickerErrorLoadDomains": "加载组织域名失败",
|
||||||
|
|||||||
@@ -89,12 +89,8 @@ export const sites = pgTable("sites", {
|
|||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
megabytesIn: real("bytesIn").default(0),
|
|
||||||
megabytesOut: real("bytesOut").default(0),
|
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
|
||||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
address: varchar("address"),
|
address: varchar("address"),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
@@ -729,10 +725,7 @@ export const clients = pgTable("clients", {
|
|||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet").notNull(),
|
subnet: varchar("subnet").notNull(),
|
||||||
megabytesIn: real("bytesIn"),
|
|
||||||
megabytesOut: real("bytesOut"),
|
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
type: varchar("type").notNull(), // "olm"
|
type: varchar("type").notNull(), // "olm"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
// endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
@@ -745,6 +738,42 @@ export const clients = pgTable("clients", {
|
|||||||
>()
|
>()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sitePing = pgTable("sitePing", {
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
lastPing: integer("lastPing")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteBandwidth = pgTable("siteBandwidth", {
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
megabytesIn: real("bytesIn").default(0),
|
||||||
|
megabytesOut: real("bytesOut").default(0),
|
||||||
|
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientPing = pgTable("clientPing", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
lastPing: integer("lastPing")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientBandwidth = pgTable("clientBandwidth", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
megabytesIn: real("bytesIn"),
|
||||||
|
megabytesOut: real("bytesOut"),
|
||||||
|
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||||
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = pgTable(
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
"clientSitesAssociationsCache",
|
"clientSitesAssociationsCache",
|
||||||
{
|
{
|
||||||
@@ -1106,3 +1135,7 @@ export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
|||||||
export type RoundTripMessageTracker = InferSelectModel<
|
export type RoundTripMessageTracker = InferSelectModel<
|
||||||
typeof roundTripMessageTracker
|
typeof roundTripMessageTracker
|
||||||
>;
|
>;
|
||||||
|
export type SitePing = typeof sitePing.$inferSelect;
|
||||||
|
export type SiteBandwidth = typeof siteBandwidth.$inferSelect;
|
||||||
|
export type ClientPing = typeof clientPing.$inferSelect;
|
||||||
|
export type ClientBandwidth = typeof clientBandwidth.$inferSelect;
|
||||||
|
|||||||
@@ -95,12 +95,8 @@ export const sites = sqliteTable("sites", {
|
|||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
megabytesIn: integer("bytesIn").default(0),
|
|
||||||
megabytesOut: integer("bytesOut").default(0),
|
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
|
||||||
type: text("type").notNull(), // "newt" or "wireguard"
|
type: text("type").notNull(), // "newt" or "wireguard"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
|
|
||||||
// exit node stuff that is how to connect to the site when it has a wg server
|
// exit node stuff that is how to connect to the site when it has a wg server
|
||||||
address: text("address"), // this is the address of the wireguard interface in newt
|
address: text("address"), // this is the address of the wireguard interface in newt
|
||||||
@@ -399,10 +395,7 @@ export const clients = sqliteTable("clients", {
|
|||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
subnet: text("subnet").notNull(),
|
subnet: text("subnet").notNull(),
|
||||||
megabytesIn: integer("bytesIn"),
|
|
||||||
megabytesOut: integer("bytesOut"),
|
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
type: text("type").notNull(), // "olm"
|
type: text("type").notNull(), // "olm"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
// endpoint: text("endpoint"),
|
// endpoint: text("endpoint"),
|
||||||
@@ -414,6 +407,42 @@ export const clients = sqliteTable("clients", {
|
|||||||
>()
|
>()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sitePing = sqliteTable("sitePing", {
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
lastPing: integer("lastPing")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteBandwidth = sqliteTable("siteBandwidth", {
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
megabytesIn: integer("bytesIn").default(0),
|
||||||
|
megabytesOut: integer("bytesOut").default(0),
|
||||||
|
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientPing = sqliteTable("clientPing", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
lastPing: integer("lastPing")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientBandwidth = sqliteTable("clientBandwidth", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
megabytesIn: integer("bytesIn"),
|
||||||
|
megabytesOut: integer("bytesOut"),
|
||||||
|
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||||
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = sqliteTable(
|
export const clientSitesAssociationsCache = sqliteTable(
|
||||||
"clientSitesAssociationsCache",
|
"clientSitesAssociationsCache",
|
||||||
{
|
{
|
||||||
@@ -1209,3 +1238,7 @@ export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
|||||||
export type RoundTripMessageTracker = InferSelectModel<
|
export type RoundTripMessageTracker = InferSelectModel<
|
||||||
typeof roundTripMessageTracker
|
typeof roundTripMessageTracker
|
||||||
>;
|
>;
|
||||||
|
export type SitePing = typeof sitePing.$inferSelect;
|
||||||
|
export type SiteBandwidth = typeof siteBandwidth.$inferSelect;
|
||||||
|
export type ClientPing = typeof clientPing.$inferSelect;
|
||||||
|
export type ClientBandwidth = typeof clientBandwidth.$inferSelect;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import config from "./config";
|
|||||||
import { getHostMeta } from "./hostMeta";
|
import { getHostMeta } from "./hostMeta";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { apiKeys, db, roles, siteResources } from "@server/db";
|
import { apiKeys, db, roles, siteResources } from "@server/db";
|
||||||
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
import { sites, users, orgs, resources, clients, idp, siteBandwidth } from "@server/db";
|
||||||
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
|
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
|
||||||
import { APP_VERSION } from "./consts";
|
import { APP_VERSION } from "./consts";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
@@ -150,12 +150,13 @@ class TelemetryClient {
|
|||||||
const siteDetails = await db
|
const siteDetails = await db
|
||||||
.select({
|
.select({
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
megabytesIn: sites.megabytesIn,
|
megabytesIn: siteBandwidth.megabytesIn,
|
||||||
megabytesOut: sites.megabytesOut,
|
megabytesOut: siteBandwidth.megabytesOut,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online
|
online: sites.online
|
||||||
})
|
})
|
||||||
.from(sites);
|
.from(sites)
|
||||||
|
.leftJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId));
|
||||||
|
|
||||||
const supporterKey = config.getSupporterData();
|
const supporterKey = config.getSupporterData();
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import {
|
|||||||
subscriptionItems,
|
subscriptionItems,
|
||||||
usage,
|
usage,
|
||||||
sites,
|
sites,
|
||||||
|
siteBandwidth,
|
||||||
customers,
|
customers,
|
||||||
orgs
|
orgs
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
|
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
|
||||||
import stripe from "#private/lib/stripe";
|
import stripe from "#private/lib/stripe";
|
||||||
@@ -253,14 +254,19 @@ export async function handleSubscriptionUpdated(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also reset the sites to 0
|
// Also reset the site bandwidth to 0
|
||||||
await trx
|
await trx
|
||||||
.update(sites)
|
.update(siteBandwidth)
|
||||||
.set({
|
.set({
|
||||||
megabytesIn: 0,
|
megabytesIn: 0,
|
||||||
megabytesOut: 0
|
megabytesOut: 0
|
||||||
})
|
})
|
||||||
.where(eq(sites.orgId, orgId));
|
.where(
|
||||||
|
inArray(
|
||||||
|
siteBandwidth.siteId,
|
||||||
|
trx.select({ siteId: sites.siteId }).from(sites).where(eq(sites.orgId, orgId))
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
clientBandwidth,
|
||||||
clients,
|
clients,
|
||||||
clientSitesAssociationsCache,
|
clientSitesAssociationsCache,
|
||||||
currentFingerprint,
|
currentFingerprint,
|
||||||
@@ -180,8 +181,8 @@ function queryClientsBase() {
|
|||||||
name: clients.name,
|
name: clients.name,
|
||||||
pubKey: clients.pubKey,
|
pubKey: clients.pubKey,
|
||||||
subnet: clients.subnet,
|
subnet: clients.subnet,
|
||||||
megabytesIn: clients.megabytesIn,
|
megabytesIn: clientBandwidth.megabytesIn,
|
||||||
megabytesOut: clients.megabytesOut,
|
megabytesOut: clientBandwidth.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
type: clients.type,
|
type: clients.type,
|
||||||
online: clients.online,
|
online: clients.online,
|
||||||
@@ -200,7 +201,8 @@ function queryClientsBase() {
|
|||||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
.leftJoin(users, eq(clients.userId, users.userId))
|
.leftJoin(users, eq(clients.userId, users.userId))
|
||||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||||
|
.leftJoin(clientBandwidth, eq(clientBandwidth.clientId, clients.clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSiteAssociations(clientIds: number[]) {
|
async function getSiteAssociations(clientIds: number[]) {
|
||||||
@@ -367,9 +369,15 @@ export async function listClients(
|
|||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
sort_by
|
sort_by
|
||||||
? order === "asc"
|
? (() => {
|
||||||
? asc(clients[sort_by])
|
const field =
|
||||||
: desc(clients[sort_by])
|
sort_by === "megabytesIn"
|
||||||
|
? clientBandwidth.megabytesIn
|
||||||
|
: sort_by === "megabytesOut"
|
||||||
|
? clientBandwidth.megabytesOut
|
||||||
|
: clients.name;
|
||||||
|
return order === "asc" ? asc(field) : desc(field);
|
||||||
|
})()
|
||||||
: asc(clients.name)
|
: asc(clients.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import {
|
import {
|
||||||
|
clientBandwidth,
|
||||||
clients,
|
clients,
|
||||||
currentFingerprint,
|
currentFingerprint,
|
||||||
db,
|
db,
|
||||||
@@ -211,8 +212,8 @@ function queryUserDevicesBase() {
|
|||||||
name: clients.name,
|
name: clients.name,
|
||||||
pubKey: clients.pubKey,
|
pubKey: clients.pubKey,
|
||||||
subnet: clients.subnet,
|
subnet: clients.subnet,
|
||||||
megabytesIn: clients.megabytesIn,
|
megabytesIn: clientBandwidth.megabytesIn,
|
||||||
megabytesOut: clients.megabytesOut,
|
megabytesOut: clientBandwidth.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
type: clients.type,
|
type: clients.type,
|
||||||
online: clients.online,
|
online: clients.online,
|
||||||
@@ -239,7 +240,8 @@ function queryUserDevicesBase() {
|
|||||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
.leftJoin(users, eq(clients.userId, users.userId))
|
.leftJoin(users, eq(clients.userId, users.userId))
|
||||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||||
|
.leftJoin(clientBandwidth, eq(clientBandwidth.clientId, clients.clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
type OlmWithUpdateAvailable = Awaited<
|
type OlmWithUpdateAvailable = Awaited<
|
||||||
@@ -427,9 +429,15 @@ export async function listUserDevices(
|
|||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
sort_by
|
sort_by
|
||||||
? order === "asc"
|
? (() => {
|
||||||
? asc(clients[sort_by])
|
const field =
|
||||||
: desc(clients[sort_by])
|
sort_by === "megabytesIn"
|
||||||
|
? clientBandwidth.megabytesIn
|
||||||
|
: sort_by === "megabytesOut"
|
||||||
|
? clientBandwidth.megabytesOut
|
||||||
|
: clients.name;
|
||||||
|
return order === "asc" ? asc(field) : desc(field);
|
||||||
|
})()
|
||||||
: asc(clients.clientId)
|
: asc(clients.clientId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
|||||||
const snapshot = accumulator;
|
const snapshot = accumulator;
|
||||||
accumulator = new Map<string, AccumulatorEntry>();
|
accumulator = new Map<string, AccumulatorEntry>();
|
||||||
|
|
||||||
const currentTime = new Date().toISOString();
|
const currentEpoch = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -157,33 +157,52 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
pubKey: string;
|
pubKey: string;
|
||||||
}>(sql`
|
}>(sql`
|
||||||
UPDATE sites
|
WITH upsert AS (
|
||||||
SET
|
INSERT INTO "siteBandwidth" ("siteId", "bytesIn", "bytesOut", "lastBandwidthUpdate")
|
||||||
"bytesOut" = COALESCE("bytesOut", 0) + ${bytesIn},
|
SELECT s."siteId", ${bytesIn}, ${bytesOut}, ${currentEpoch}
|
||||||
"bytesIn" = COALESCE("bytesIn", 0) + ${bytesOut},
|
FROM "sites" s WHERE s."pubKey" = ${publicKey}
|
||||||
"lastBandwidthUpdate" = ${currentTime}
|
ON CONFLICT ("siteId") DO UPDATE SET
|
||||||
WHERE "pubKey" = ${publicKey}
|
"bytesIn" = COALESCE("siteBandwidth"."bytesIn", 0) + EXCLUDED."bytesIn",
|
||||||
RETURNING "orgId", "pubKey"
|
"bytesOut" = COALESCE("siteBandwidth"."bytesOut", 0) + EXCLUDED."bytesOut",
|
||||||
|
"lastBandwidthUpdate" = EXCLUDED."lastBandwidthUpdate"
|
||||||
|
RETURNING "siteId"
|
||||||
|
)
|
||||||
|
SELECT u."siteId", s."orgId", s."pubKey"
|
||||||
|
FROM upsert u
|
||||||
|
INNER JOIN "sites" s ON s."siteId" = u."siteId"
|
||||||
`);
|
`);
|
||||||
results.push(...result);
|
results.push(...result);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
|
// PostgreSQL: batch UPSERT via CTE — 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)`
|
||||||
);
|
);
|
||||||
const valuesClause = sql.join(valuesList, sql`, `);
|
const valuesClause = sql.join(valuesList, sql`, `);
|
||||||
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
|
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
|
||||||
UPDATE sites
|
WITH vals(pub_key, bytes_in, bytes_out) AS (
|
||||||
SET
|
VALUES ${valuesClause}
|
||||||
"bytesOut" = COALESCE("bytesOut", 0) + v.bytes_in,
|
),
|
||||||
"bytesIn" = COALESCE("bytesIn", 0) + v.bytes_out,
|
site_lookup AS (
|
||||||
"lastBandwidthUpdate" = ${currentTime}
|
SELECT s."siteId", s."orgId", s."pubKey", v.bytes_in, v.bytes_out
|
||||||
FROM (VALUES ${valuesClause}) AS v(pub_key, bytes_in, bytes_out)
|
FROM vals v
|
||||||
WHERE sites."pubKey" = v.pub_key
|
INNER JOIN "sites" s ON s."pubKey" = v.pub_key
|
||||||
RETURNING sites."orgId" AS "orgId", sites."pubKey" AS "pubKey"
|
),
|
||||||
|
upsert AS (
|
||||||
|
INSERT INTO "siteBandwidth" ("siteId", "bytesIn", "bytesOut", "lastBandwidthUpdate")
|
||||||
|
SELECT sl."siteId", sl.bytes_in, sl.bytes_out, ${currentEpoch}::integer
|
||||||
|
FROM site_lookup sl
|
||||||
|
ON CONFLICT ("siteId") DO UPDATE SET
|
||||||
|
"bytesIn" = COALESCE("siteBandwidth"."bytesIn", 0) + EXCLUDED."bytesIn",
|
||||||
|
"bytesOut" = COALESCE("siteBandwidth"."bytesOut", 0) + EXCLUDED."bytesOut",
|
||||||
|
"lastBandwidthUpdate" = EXCLUDED."lastBandwidthUpdate"
|
||||||
|
RETURNING "siteId"
|
||||||
|
)
|
||||||
|
SELECT u."siteId", s."orgId", s."pubKey"
|
||||||
|
FROM upsert u
|
||||||
|
INNER JOIN "sites" s ON s."siteId" = u."siteId"
|
||||||
`);
|
`);
|
||||||
}, `flush bandwidth chunk [${i}–${chunkEnd}]`);
|
}, `flush bandwidth chunk [${i}–${chunkEnd}]`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { db, newts, sites, targetHealthCheck, targets } from "@server/db";
|
import { db, newts, sites, targetHealthCheck, targets, sitePing, siteBandwidth } from "@server/db";
|
||||||
import {
|
import {
|
||||||
hasActiveConnections,
|
hasActiveConnections,
|
||||||
getClientConfigVersion
|
getClientConfigVersion
|
||||||
} from "#dynamic/routers/ws";
|
} from "#dynamic/routers/ws";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { Newt } from "@server/db";
|
import { Newt } from "@server/db";
|
||||||
import { eq, lt, isNull, and, or, ne, not } from "drizzle-orm";
|
import { eq, lt, isNull, and, or, ne } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendNewtSyncMessage } from "./sync";
|
import { sendNewtSyncMessage } from "./sync";
|
||||||
import { recordPing } from "./pingAccumulator";
|
import { recordPing } from "./pingAccumulator";
|
||||||
@@ -41,17 +41,18 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
newtId: newts.newtId,
|
newtId: newts.newtId,
|
||||||
lastPing: sites.lastPing
|
lastPing: sitePing.lastPing
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
||||||
|
.leftJoin(sitePing, eq(sitePing.siteId, sites.siteId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sites.online, true),
|
eq(sites.online, true),
|
||||||
eq(sites.type, "newt"),
|
eq(sites.type, "newt"),
|
||||||
or(
|
or(
|
||||||
lt(sites.lastPing, twoMinutesAgo),
|
lt(sitePing.lastPing, twoMinutesAgo),
|
||||||
isNull(sites.lastPing)
|
isNull(sitePing.lastPing)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -112,15 +113,11 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
lastBandwidthUpdate: siteBandwidth.lastBandwidthUpdate
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.innerJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId))
|
||||||
and(
|
.where(eq(sites.type, "wireguard"));
|
||||||
eq(sites.type, "wireguard"),
|
|
||||||
not(isNull(sites.lastBandwidthUpdate))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const wireguardOfflineThreshold = Math.floor(
|
const wireguardOfflineThreshold = Math.floor(
|
||||||
(Date.now() - OFFLINE_THRESHOLD_BANDWIDTH_MS) / 1000
|
(Date.now() - OFFLINE_THRESHOLD_BANDWIDTH_MS) / 1000
|
||||||
@@ -128,12 +125,7 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
|
|
||||||
// loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline
|
// loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline
|
||||||
for (const site of allWireguardSites) {
|
for (const site of allWireguardSites) {
|
||||||
const lastBandwidthUpdate =
|
if ((site.lastBandwidthUpdate ?? 0) < wireguardOfflineThreshold && site.online) {
|
||||||
new Date(site.lastBandwidthUpdate!).getTime() / 1000;
|
|
||||||
if (
|
|
||||||
lastBandwidthUpdate < wireguardOfflineThreshold &&
|
|
||||||
site.online
|
|
||||||
) {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking wireguard site ${site.siteId} offline: no bandwidth update in over ${OFFLINE_THRESHOLD_BANDWIDTH_MS / 60000} minutes`
|
`Marking wireguard site ${site.siteId} offline: no bandwidth update in over ${OFFLINE_THRESHOLD_BANDWIDTH_MS / 60000} minutes`
|
||||||
);
|
);
|
||||||
@@ -142,10 +134,7 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.update(sites)
|
.update(sites)
|
||||||
.set({ online: false })
|
.set({ online: false })
|
||||||
.where(eq(sites.siteId, site.siteId));
|
.where(eq(sites.siteId, site.siteId));
|
||||||
} else if (
|
} else if ((site.lastBandwidthUpdate ?? 0) >= wireguardOfflineThreshold && !site.online) {
|
||||||
lastBandwidthUpdate >= wireguardOfflineThreshold &&
|
|
||||||
!site.online
|
|
||||||
) {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking wireguard site ${site.siteId} online: recent bandwidth update`
|
`Marking wireguard site ${site.siteId} online: recent bandwidth update`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { db } from "@server/db";
|
import { db, clients, clientBandwidth } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { clients } from "@server/db";
|
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ export async function flushBandwidthToDb(): Promise<void> {
|
|||||||
const snapshot = accumulator;
|
const snapshot = accumulator;
|
||||||
accumulator = new Map<string, BandwidthAccumulator>();
|
accumulator = new Map<string, BandwidthAccumulator>();
|
||||||
|
|
||||||
const currentTime = new Date().toISOString();
|
const currentEpoch = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -101,19 +100,37 @@ export async function flushBandwidthToDb(): Promise<void> {
|
|||||||
for (const [publicKey, { bytesIn, bytesOut }] of sortedEntries) {
|
for (const [publicKey, { bytesIn, bytesOut }] of sortedEntries) {
|
||||||
try {
|
try {
|
||||||
await withDeadlockRetry(async () => {
|
await withDeadlockRetry(async () => {
|
||||||
// Use atomic SQL increment to avoid the SELECT-then-UPDATE
|
// Find clientId by pubKey
|
||||||
// anti-pattern and the races it would introduce.
|
const [clientRow] = await db
|
||||||
|
.select({ clientId: clients.clientId })
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.pubKey, publicKey))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!clientRow) {
|
||||||
|
logger.warn(`No client found for pubKey ${publicKey}, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(clients)
|
.insert(clientBandwidth)
|
||||||
.set({
|
.values({
|
||||||
|
clientId: clientRow.clientId,
|
||||||
// Note: bytesIn from peer goes to megabytesOut (data
|
// Note: bytesIn from peer goes to megabytesOut (data
|
||||||
// sent to client) and bytesOut from peer goes to
|
// sent to client) and bytesOut from peer goes to
|
||||||
// megabytesIn (data received from client).
|
// megabytesIn (data received from client).
|
||||||
megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`,
|
megabytesOut: bytesIn,
|
||||||
megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`,
|
megabytesIn: bytesOut,
|
||||||
lastBandwidthUpdate: currentTime
|
lastBandwidthUpdate: currentEpoch
|
||||||
})
|
})
|
||||||
.where(eq(clients.pubKey, publicKey));
|
.onConflictDoUpdate({
|
||||||
|
target: clientBandwidth.clientId,
|
||||||
|
set: {
|
||||||
|
megabytesOut: sql`COALESCE(${clientBandwidth.megabytesOut}, 0) + ${bytesIn}`,
|
||||||
|
megabytesIn: sql`COALESCE(${clientBandwidth.megabytesIn}, 0) + ${bytesOut}`,
|
||||||
|
lastBandwidthUpdate: currentEpoch
|
||||||
|
}
|
||||||
|
});
|
||||||
}, `flush bandwidth for client ${publicKey}`);
|
}, `flush bandwidth for client ${publicKey}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { sites, clients, olms } from "@server/db";
|
import { sites, clients, olms, sitePing, clientPing } from "@server/db";
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray, sql } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,11 +81,8 @@ export function recordClientPing(
|
|||||||
/**
|
/**
|
||||||
* Flush all accumulated site pings to the database.
|
* Flush all accumulated site pings to the database.
|
||||||
*
|
*
|
||||||
* Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE
|
* For each batch: first upserts individual per-site timestamps into
|
||||||
* statement. We use the maximum timestamp across the batch so that `lastPing`
|
* `sitePing`, then bulk-updates `sites.online = true`.
|
||||||
* 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> {
|
async function flushSitePingsToDb(): Promise<void> {
|
||||||
if (pendingSitePings.size === 0) {
|
if (pendingSitePings.size === 0) {
|
||||||
@@ -103,20 +100,25 @@ async function flushSitePingsToDb(): Promise<void> {
|
|||||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||||
const batch = entries.slice(i, 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);
|
const siteIds = batch.map(([id]) => id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withRetry(async () => {
|
await withRetry(async () => {
|
||||||
|
const rows = batch.map(([siteId, ts]) => ({ siteId, lastPing: ts }));
|
||||||
|
|
||||||
|
// Step 1: Upsert ping timestamps into sitePing
|
||||||
|
await db
|
||||||
|
.insert(sitePing)
|
||||||
|
.values(rows)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: sitePing.siteId,
|
||||||
|
set: { lastPing: sql`excluded."lastPing"` }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Update online status on sites
|
||||||
await db
|
await db
|
||||||
.update(sites)
|
.update(sites)
|
||||||
.set({
|
.set({ online: true })
|
||||||
online: true,
|
|
||||||
lastPing: maxTimestamp
|
|
||||||
})
|
|
||||||
.where(inArray(sites.siteId, siteIds));
|
.where(inArray(sites.siteId, siteIds));
|
||||||
}, "flushSitePingsToDb");
|
}, "flushSitePingsToDb");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -139,7 +141,8 @@ async function flushSitePingsToDb(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* Flush all accumulated client (OLM) pings to the database.
|
* Flush all accumulated client (OLM) pings to the database.
|
||||||
*
|
*
|
||||||
* Same single-UPDATE-per-batch approach as `flushSitePingsToDb`.
|
* For each batch: first upserts individual per-client timestamps into
|
||||||
|
* `clientPing`, then bulk-updates `clients.online = true, archived = false`.
|
||||||
*/
|
*/
|
||||||
async function flushClientPingsToDb(): Promise<void> {
|
async function flushClientPingsToDb(): Promise<void> {
|
||||||
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
|
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
|
||||||
@@ -161,18 +164,25 @@ async function flushClientPingsToDb(): Promise<void> {
|
|||||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||||
const batch = entries.slice(i, 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);
|
const clientIds = batch.map(([id]) => id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withRetry(async () => {
|
await withRetry(async () => {
|
||||||
|
const rows = batch.map(([clientId, ts]) => ({ clientId, lastPing: ts }));
|
||||||
|
|
||||||
|
// Step 1: Upsert ping timestamps into clientPing
|
||||||
|
await db
|
||||||
|
.insert(clientPing)
|
||||||
|
.values(rows)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: clientPing.clientId,
|
||||||
|
set: { lastPing: sql`excluded."lastPing"` }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Update online + unarchive on clients
|
||||||
await db
|
await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({ online: true, archived: false })
|
||||||
lastPing: maxTimestamp,
|
|
||||||
online: true,
|
|
||||||
archived: false
|
|
||||||
})
|
|
||||||
.where(inArray(clients.clientId, clientIds));
|
.where(inArray(clients.clientId, clientIds));
|
||||||
}, "flushClientPingsToDb");
|
}, "flushClientPingsToDb");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { clients, olms, Olm } from "@server/db";
|
import { clients, olms, Olm, clientPing } from "@server/db";
|
||||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
import { eq, lt, isNull, and, or, inArray } from "drizzle-orm";
|
||||||
import { recordClientPing } from "@server/routers/newt/pingAccumulator";
|
import { recordClientPing } from "@server/routers/newt/pingAccumulator";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
@@ -37,21 +37,33 @@ export const startOlmOfflineChecker = (): void => {
|
|||||||
// TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
|
// TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
|
||||||
|
|
||||||
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
||||||
const offlineClients = await db
|
const staleClientRows = await db
|
||||||
.update(clients)
|
.select({
|
||||||
.set({ online: false })
|
clientId: clients.clientId,
|
||||||
|
olmId: clients.olmId,
|
||||||
|
lastPing: clientPing.lastPing
|
||||||
|
})
|
||||||
|
.from(clients)
|
||||||
|
.leftJoin(clientPing, eq(clientPing.clientId, clients.clientId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(clients.online, true),
|
eq(clients.online, true),
|
||||||
or(
|
or(
|
||||||
lt(clients.lastPing, twoMinutesAgo),
|
lt(clientPing.lastPing, twoMinutesAgo),
|
||||||
isNull(clients.lastPing)
|
isNull(clientPing.lastPing)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.returning();
|
|
||||||
|
|
||||||
for (const offlineClient of offlineClients) {
|
if (staleClientRows.length > 0) {
|
||||||
|
const staleClientIds = staleClientRows.map((c) => c.clientId);
|
||||||
|
await db
|
||||||
|
.update(clients)
|
||||||
|
.set({ online: false })
|
||||||
|
.where(inArray(clients.clientId, staleClientIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const offlineClient of staleClientRows) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Kicking offline olm client ${offlineClient.clientId} due to inactivity`
|
`Kicking offline olm client ${offlineClient.clientId} due to inactivity`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, sites } from "@server/db";
|
import { db, sites, siteBandwidth } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
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";
|
||||||
@@ -60,12 +60,17 @@ export async function resetOrgBandwidth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(sites)
|
.update(siteBandwidth)
|
||||||
.set({
|
.set({
|
||||||
megabytesIn: 0,
|
megabytesIn: 0,
|
||||||
megabytesOut: 0
|
megabytesOut: 0
|
||||||
})
|
})
|
||||||
.where(eq(sites.orgId, orgId));
|
.where(
|
||||||
|
inArray(
|
||||||
|
siteBandwidth.siteId,
|
||||||
|
db.select({ siteId: sites.siteId }).from(sites).where(eq(sites.orgId, orgId))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {},
|
data: {},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
remoteExitNodes,
|
remoteExitNodes,
|
||||||
roleSites,
|
roleSites,
|
||||||
sites,
|
sites,
|
||||||
|
siteBandwidth,
|
||||||
userSites
|
userSites
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import cache from "#dynamic/lib/cache";
|
import cache from "#dynamic/lib/cache";
|
||||||
@@ -155,8 +156,8 @@ function querySitesBase() {
|
|||||||
name: sites.name,
|
name: sites.name,
|
||||||
pubKey: sites.pubKey,
|
pubKey: sites.pubKey,
|
||||||
subnet: sites.subnet,
|
subnet: sites.subnet,
|
||||||
megabytesIn: sites.megabytesIn,
|
megabytesIn: siteBandwidth.megabytesIn,
|
||||||
megabytesOut: sites.megabytesOut,
|
megabytesOut: siteBandwidth.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
@@ -175,7 +176,8 @@ function querySitesBase() {
|
|||||||
.leftJoin(
|
.leftJoin(
|
||||||
remoteExitNodes,
|
remoteExitNodes,
|
||||||
eq(remoteExitNodes.exitNodeId, sites.exitNodeId)
|
eq(remoteExitNodes.exitNodeId, sites.exitNodeId)
|
||||||
);
|
)
|
||||||
|
.leftJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId));
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteWithUpdateAvailable = Awaited<ReturnType<typeof querySitesBase>>[0] & {
|
type SiteWithUpdateAvailable = Awaited<ReturnType<typeof querySitesBase>>[0] & {
|
||||||
@@ -299,9 +301,15 @@ export async function listSites(
|
|||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
sort_by
|
sort_by
|
||||||
? order === "asc"
|
? (() => {
|
||||||
? asc(sites[sort_by])
|
const field =
|
||||||
: desc(sites[sort_by])
|
sort_by === "megabytesIn"
|
||||||
|
? siteBandwidth.megabytesIn
|
||||||
|
: sort_by === "megabytesOut"
|
||||||
|
? siteBandwidth.megabytesOut
|
||||||
|
: sites.name;
|
||||||
|
return order === "asc" ? asc(field) : desc(field);
|
||||||
|
})()
|
||||||
: asc(sites.name)
|
: asc(sites.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import m13 from "./scriptsPg/1.15.3";
|
|||||||
import m14 from "./scriptsPg/1.15.4";
|
import m14 from "./scriptsPg/1.15.4";
|
||||||
import m15 from "./scriptsPg/1.16.0";
|
import m15 from "./scriptsPg/1.16.0";
|
||||||
import m16 from "./scriptsPg/1.17.0";
|
import m16 from "./scriptsPg/1.17.0";
|
||||||
|
import m17 from "./scriptsPg/1.18.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -43,7 +44,8 @@ const migrations = [
|
|||||||
{ version: "1.15.3", run: m13 },
|
{ version: "1.15.3", run: m13 },
|
||||||
{ version: "1.15.4", run: m14 },
|
{ version: "1.15.4", run: m14 },
|
||||||
{ version: "1.16.0", run: m15 },
|
{ version: "1.16.0", run: m15 },
|
||||||
{ version: "1.17.0", run: m16 }
|
{ version: "1.17.0", run: m16 },
|
||||||
|
{ version: "1.18.0", run: m17 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import m34 from "./scriptsSqlite/1.15.3";
|
|||||||
import m35 from "./scriptsSqlite/1.15.4";
|
import m35 from "./scriptsSqlite/1.15.4";
|
||||||
import m36 from "./scriptsSqlite/1.16.0";
|
import m36 from "./scriptsSqlite/1.16.0";
|
||||||
import m37 from "./scriptsSqlite/1.17.0";
|
import m37 from "./scriptsSqlite/1.17.0";
|
||||||
|
import m38 from "./scriptsSqlite/1.18.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -77,7 +78,8 @@ const migrations = [
|
|||||||
{ version: "1.15.3", run: m34 },
|
{ version: "1.15.3", run: m34 },
|
||||||
{ version: "1.15.4", run: m35 },
|
{ version: "1.15.4", run: m35 },
|
||||||
{ version: "1.16.0", run: m36 },
|
{ version: "1.16.0", run: m36 },
|
||||||
{ version: "1.17.0", run: m37 }
|
{ version: "1.17.0", run: m37 },
|
||||||
|
{ version: "1.18.0", run: m38 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user