Browse Source

WIP new WebAdmin: status page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 1 year ago
parent
commit
9fcff83f8f

+ 3 - 3
go.mod

@@ -3,7 +3,7 @@ module github.com/drakkan/sftpgo/v2
 go 1.21
 
 require (
-	cloud.google.com/go/storage v1.36.0
+	cloud.google.com/go/storage v1.37.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
@@ -13,9 +13,9 @@ require (
 	github.com/aws/aws-sdk-go-v2/config v1.26.6
 	github.com/aws/aws-sdk-go-v2/credentials v1.16.16
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.15
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.48.1
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
 	github.com/aws/aws-sdk-go-v2/service/sts v1.26.7
 	github.com/bmatcuk/doublestar/v4 v4.6.1

+ 6 - 6
go.sum

@@ -9,8 +9,8 @@ cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
 cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
 cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM=
 cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI=
-cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
-cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4=
+cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
@@ -43,8 +43,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5g
 github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14 h1:ogP1WgyvN/qxPJkgtFMD7G2eKb5p/61Jomx+nIHXUQ4=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14/go.mod h1:nYd/WmIrXlBHW/5QwrZP81/Gz08wKi87nV6EI1kmqx4=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.15 h1:2MUXyGW6dVaQz6aqycpbdLIH1NMcUI6kW6vQ0RabGYg=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.15/go.mod h1:aHbhbR6WEQgHAiRj41EQ2W47yOYwNtIkWTXmcAtYqj8=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
@@ -63,8 +63,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 h1:KOxnQeWy5sXyS
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10/go.mod h1:jMx5INQFYFYB3lQD9W0D8Ohgq6Wnl7NYOJ2TQndbulI=
 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6 h1:JWy+uLKZQR/9a3gQ+jQa28FEJ/41Z0spdbbQodaXFeA=
 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6/go.mod h1:T2NcfuIuXWcuwVwg3rBIW6h1cfzCdrzSn4Hs0KltND8=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflYDHFoZAu/sKYkwU=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.48.1 h1:5XNlsBsEvBZBMO6p82y+sqpWg8j5aBCe+5C2GBFgqBQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.48.1/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU=
 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1vkSIWAN8tPwPCoYZBp7aruR540=
 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
 github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=

+ 5 - 5
internal/ftpd/ftpd.go

@@ -228,19 +228,19 @@ func (b *Binding) HasProxy() bool {
 // GetTLSDescription returns the TLS mode as string
 func (b *Binding) GetTLSDescription() string {
 	if certMgr == nil {
-		return "Disabled"
+		return util.I18nFTPTLSDisabled
 	}
 	switch b.TLSMode {
 	case 1:
-		return "Explicit required"
+		return util.I18nFTPTLSExplicit
 	case 2:
-		return "Implicit"
+		return util.I18nFTPTLSImplicit
 	}
 
 	if certMgr.HasCertificate(common.DefaultTLSKeyPaidID) || certMgr.HasCertificate(b.GetAddress()) {
-		return "Plain and explicit"
+		return util.I18nFTPTLSMixed
 	}
-	return "Disabled"
+	return util.I18nFTPTLSDisabled
 }
 
 // PortRange defines a port range

+ 2 - 3
internal/httpd/webadmin.go

@@ -98,7 +98,6 @@ const (
 	templateMaintenance      = "maintenance.html"
 	templateMFA              = "mfa.html"
 	templateSetup            = "adminsetup.html"
-	pageStatusTitle          = "Status"
 	pageEventRulesTitle      = "Event rules"
 	pageEventActionsTitle    = "Event actions"
 	pageMaintenanceTitle     = "Maintenance"
@@ -442,7 +441,7 @@ func loadAdminTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateAdminDir, templateEventAction),
 	}
 	statusPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 		filepath.Join(templatesPath, templateAdminDir, templateStatus),
 	}
@@ -3311,7 +3310,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
 func (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	data := statusPage{
-		basePage: s.getBasePageData(pageStatusTitle, webStatusPath, r),
+		basePage: s.getBasePageData(util.I18nStatusTitle, webStatusPath, r),
 		Status:   getServicesStatus(),
 	}
 	renderAdminTemplate(w, templateStatus, data)

+ 5 - 0
internal/util/i18n.go

@@ -67,6 +67,7 @@ const (
 	I18nAddIPListTitle                 = "title.add_ip_list"
 	I18nUpdateIPListTitle              = "title.update_ip_list"
 	I18nDefenderTitle                  = "title.defender"
+	I18nStatusTitle                    = "status.desc"
 	I18nErrorSetupInstallCode          = "setup.install_code_mismatch"
 	I18nInvalidAuth                    = "general.invalid_auth_request"
 	I18nError429Message                = "general.error429"
@@ -225,6 +226,10 @@ const (
 	I18nErrorAdminSelfRole             = "admin.self_role"
 	I18nErrorIpInvalid                 = "ip_list.ip_invalid"
 	I18nErrorNetInvalid                = "ip_list.net_invalid"
+	I18nFTPTLSDisabled                 = "status.tls_disabled"
+	I18nFTPTLSExplicit                 = "status.tls_explicit"
+	I18nFTPTLSImplicit                 = "status.tls_implicit"
+	I18nFTPTLSMixed                    = "status.tls_mixed"
 )
 
 // NewI18nError returns a I18nError wrappring the provided error

+ 1 - 1
internal/vfs/vfs.go

@@ -189,7 +189,7 @@ type PipeWriter interface {
 	GetWrittenBytes() int64
 }
 
-// PipeReader defines an interface representing a SFTPGo pipe writer
+// PipeReader defines an interface representing a SFTPGo pipe reader
 type PipeReader interface {
 	io.Reader
 	io.ReaderAt

+ 32 - 1
static/locales/en/translation.json

@@ -234,7 +234,10 @@
         "last_login": "Last login",
         "previous": "Previous",
         "next": "Next",
-        "type": "Type"
+        "type": "Type",
+        "issuer": "Issuer",
+        "data_provider": "Database",
+        "driver": "Driver"
     },
     "fs": {
         "view_file": "View file \"{{- path}}\"",
@@ -741,5 +744,33 @@
         "ip": "IP address",
         "ban_time": "Blocked until",
         "score": "Score"
+    },
+    "status": {
+        "desc": "Status of services",
+        "ssh": "SSH/SFTP server",
+        "active": "Status: active",
+        "disabled": "Status: disabled",
+        "proxy_on": "PROXY protocol enabled",
+        "address": "Address",
+        "ssh_auths": "Authentication methods",
+        "ssh_commands": "Accepted commands",
+        "host_key": "Host key",
+        "fingeprint": "Fingerprint",
+        "algorithms": "Algorithms",
+        "algorithm": "Algorithm",
+        "ssh_pub_key_algo": "Public key authentication algorithms",
+        "ssh_mac_algo": "Message authentication code (MAC) algorithms",
+        "ssh_kex_algo": "Key exchange (KEX) algorithms",
+        "ssh_cipher_algo": "Ciphers",
+        "ftp": "FTP server",
+        "ftp_passive_range": "Passive mode port range",
+        "ftp_passive_ip": "Passive IP",
+        "tls": "TLS",
+        "tls_disabled": "Disabled",
+        "tls_explicit": "Explicit mode required (FTPES)",
+        "tls_implicit": "Implicit mode (FTPS), deprecated, prefer FTPES",
+        "tls_mixed": "Plain and explicit (FTPES) mode",
+        "webdav": "WebDAV server",
+        "rate_limiters": "Rate limiters"
     }
 }

+ 32 - 1
static/locales/it/translation.json

@@ -234,7 +234,10 @@
         "last_login": "Ultimo accesso",
         "previous": "Precedente",
         "next": "Successivo",
-        "type": "Tipo"
+        "type": "Tipo",
+        "issuer": "Emittente",
+        "data_provider": "Database",
+        "driver": "Driver"
     },
     "fs": {
         "view_file": "Visualizza file \"{{- path}}\"",
@@ -741,5 +744,33 @@
         "ip": "Indirizzo IP",
         "ban_time": "Bloccato fino a",
         "score": "Punteggio"
+    },
+    "status": {
+        "desc": "Stato dei servizi",
+        "ssh": "Server SSH/SFTP",
+        "active": "Stato: attivo",
+        "disabled": "Stato: disabilitato",
+        "proxy_on": "Protocollo PROXY abilitato",
+        "address": "Indirizzo",
+        "ssh_auths": "Metodi di autenticazione",
+        "ssh_commands": "Comandi accettati",
+        "host_key": "Chiave host",
+        "fingeprint": "Impronta",
+        "algorithms": "Algoritmi",
+        "algorithm": "Algoritmo",
+        "ssh_pub_key_algo": "Algoritmi per l'autenticazione con chiave pubblica",
+        "ssh_mac_algo": "Algoritmi MAC",
+        "ssh_kex_algo": "Algoritmi KEX",
+        "ssh_cipher_algo": "Cifrari",
+        "ftp": "Server FTP",
+        "ftp_passive_range": "Intervallo di porte in modalità passiva",
+        "ftp_passive_ip": "IP per FTP passivo",
+        "tls": "TLS",
+        "tls_disabled": "Disabilitato",
+        "tls_explicit": "Modalità esplicita richiesta (FTPES)",
+        "tls_implicit": "Modalità implicita (FTPS), sconsigliato, FTPES è preferibile",
+        "tls_mixed": "In chiaro e modalità esplicita (FTPES)",
+        "webdav": "Server WebDAV",
+        "rate_limiters": "Rate limiters"
     }
 }

+ 174 - 135
templates/webadmin/status.html

@@ -1,178 +1,217 @@
 <!--
-Copyright (C) 2019 Nicola Murino
+Copyright (C) 2024 Nicola Murino
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
+https://keenthemes.com/products/templates-mega-bundle
 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
+
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team (support@sftpgo.com).
 -->
 {{template "base" .}}
 
-{{define "title"}}{{.Title}}{{end}}
-
-{{define "page_body"}}
-
-<div class="card shadow mb-4">
-    <div class="card-header py-3">
-        <h6 class="m-0 font-weight-bold text-primary">Services</h6>
+{{- define "page_body"}}
+<div class="card shadow-sm">
+    <div class="card-header bg-light">
+        <h3 data-i18n="{{.Title}}" class="card-title section-title"></h3>
     </div>
     <div class="card-body">
-        <div class="card mb-4 {{ if .Status.SSH.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+
+        <div class="card">
+            <div class="card-header bg-light">
+                <h3 data-i18n="status.ssh" class="card-title section-title-inner">SSH/SFTP server</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">SFTP/SSH server</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.SSH.IsActive}}"Started"{{else}}"Stopped"{{end}}
-                    {{if .Status.SSH.IsActive}}
-                    <br>
-                    {{range .Status.SSH.Bindings}}
-                    <br>
-                    Address: "{{.GetAddress}}" {{if .HasProxy}}Proxy: ON{{end}}
-                    <br>
-                    {{end}}
-                    Accepted authentications: "{{.Status.SSH.GetSupportedAuthsAsString}}"
-                    <br>
-                    Accepted commands: "{{.Status.SSH.GetSSHCommandsAsString}}"
-                    <br>
-                    {{range .Status.SSH.HostKeys}}
-                    <br>
-                    Host Key: "{{.Path}}"
-                    <br>
-                    Fingerprint: "{{.Fingerprint}}"
-                    <br>
-                    Algorithms: "{{.GetAlgosAsString}}"
-                    <br>
-                    {{end}}
-                    <br>
-                    Public key authentication algorithms: "{{.Status.SSH.GetPublicKeysAlgosAsString}}"
-                    <br><br>
-                    Message authentication algorithms: "{{.Status.SSH.GetMACsAsString}}"
-                    <br><br>
-                    Key exchange algorithms: "{{.Status.SSH.GetKEXsAsString}}"
-                    <br><br>
-                    Ciphers: "{{.Status.SSH.GetCiphersAsString}}"
-                    <br>
-                    {{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.SSH.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
+                {{- if .Status.SSH.IsActive}}
+                <div class="d-flex flex-column">
+                    {{- range .Status.SSH.Bindings}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.address"></span> "{{.GetAddress}}"
+                    </p>
+                    {{- if .HasProxy}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.proxy_on"></span>
+                    </p>
+                    {{- end}}
+                    {{- end}}
+                </div>
+                {{- range .Status.SSH.HostKeys}}
+                <div class="d-flex flex-column mt-10">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.host_key"></span> "{{.Path}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.fingeprint"></span> "{{.Fingerprint}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.algorithms"></span> "{{.GetAlgosAsString}}"
+                    </p>
+                </div>
+                {{- end}}
+                <div class="d-flex flex-column mt-10">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_commands"></span> "{{.Status.SSH.GetSSHCommandsAsString}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_auths"></span> "{{.Status.SSH.GetSupportedAuthsAsString}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_pub_key_algo"></span> "{{.Status.SSH.GetPublicKeysAlgosAsString}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_mac_algo"></span> "{{.Status.SSH.GetMACsAsString}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_kex_algo"></span> "{{.Status.SSH.GetKEXsAsString}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ssh_cipher_algo"></span> "{{.Status.SSH.GetCiphersAsString}}"
+                    </p>
+                </div>
+                {{- end}}
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.FTP.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="status.ftp" class="card-title section-title-inner">FTP server</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">FTP server</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.FTP.IsActive}}"Started"{{else}}"Stopped"{{end}}
-                    {{if .Status.FTP.IsActive}}
-                    <br>
-                    {{range .Status.FTP.Bindings}}
-                    <br>
-                    Address: "{{.GetAddress}}" {{if .HasProxy}}Proxy: ON{{end}}
-                    <br>
-                    TLS: "{{.GetTLSDescription}}"
-                    {{if .ForcePassiveIP}}
-                    <br>
-                    Passive IP: {{.ForcePassiveIP}}
-                    {{end}}
-                    <br>
-                    {{range .PassiveIPOverrides}}
-                    Passive IP: {{.IP}} for networks: {{.GetNetworksAsString}}
-                    <br>
-                    {{end}}
-                    {{end}}
-                    <br>
-                    Passive port range: "{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}"
-                    {{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.FTP.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
+                {{- if .Status.FTP.IsActive}}
+                <div class="d-flex flex-column">
+                    {{- range .Status.FTP.Bindings}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.address"></span> "{{.GetAddress}}"
+                    </p>
+                    {{- if .HasProxy}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.proxy_on"></span>
+                    </p>
+                    {{- end}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.tls"></span>&nbsp;<span data-i18n="{{.GetTLSDescription}}"></span>
+                    </p>
+                    {{- if .ForcePassiveIP}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ftp_passive_ip"></span> "{{.ForcePassiveIP}}"
+                    </p>
+                    {{- end}}
+                    {{- range .PassiveIPOverrides}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ftp_passive_ip"></span> "{{.IP}} ({{.GetNetworksAsString}})"
+                    </p>
+                    {{- end}}
+                    {{- end}}
+                </div>
+                <div class="d-flex flex-column mt-10">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.ftp_passive_range"></span> "{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}"
+                    </p>
+                </div>
+                {{- end}}
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.WebDAV.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="status.webdav" class="card-title section-title-inner">WebDAV server</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">WebDAV server</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.WebDAV.IsActive}}"Started"{{else}}"Stopped"{{end}}
-                    {{if .Status.WebDAV.IsActive}}
-                    <br>
-                    {{range .Status.WebDAV.Bindings}}
-                    <br>
-                    Address: "{{.GetAddress}}"
-                    <br>
-                    Protocol: {{if .EnableHTTPS}} HTTPS {{else}} HTTP {{end}}
-                    <br>
-                    {{end}}
-                    {{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.WebDAV.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
+                {{- if .Status.WebDAV.IsActive}}
+                <div class="d-flex flex-column">
+                    {{- range .Status.WebDAV.Bindings}}
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.address"></span> "{{.GetAddress}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="general.protocol"></span> {{if .EnableHTTPS}} HTTPS {{else}} HTTP {{end}}
+                    </p>
+                    {{- end}}
+                </div>
+                {{- end}}
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.AllowList.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="iplist.allow_list" class="card-title section-title-inner">Allow list</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">Allow list</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.AllowList.IsActive}}"Enabled"{{else}}"Disabled"{{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.AllowList.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.Defender.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="iplist.defender_list" class="card-title section-title-inner">Defender</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">Defender</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.Defender.IsActive}}"Enabled"{{else}}"Disabled"{{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.Defender.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.RateLimiters.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="status.rate_limiters" class="card-title section-title-inner">Rate limiters</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">Rate limiters</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.RateLimiters.IsActive}}"Enabled"{{else}}"Disabled"{{end}}
-                    {{if .Status.RateLimiters.IsActive}}
-                    <br>
-                    Protocols: {{.Status.RateLimiters.GetProtocolsAsString}}
-                    {{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.RateLimiters.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
+                {{- if .Status.RateLimiters.IsActive}}
+                <div class="d-flex flex-column">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="iplist.protocols"></span> "{{.Status.RateLimiters.GetProtocolsAsString}}"
+                    </p>
+                </div>
+                {{- end}}
             </div>
         </div>
 
-        <div class="card mb-4 {{ if .Status.MFA.IsActive}}border-left-success{{else}}border-left-info{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="title.two_factor_auth" class="card-title section-title-inner">Two-factor authentication</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">Multi-factor authentication</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.MFA.IsActive}}"Enabled"{{else}}"Disabled"{{end}}
-                    {{ if .Status.MFA.IsActive}}
-                    <br>
-                    Time-based one time passwords (RFC 6238) configurations:
-                    <br>
-                    <ul>
-                    {{range .Status.MFA.TOTPConfigs}}
-                    <li>Name: "{{.Name}}", issuer: "{{.Issuer}}", HMAC algorithm: "{{.Algo}}"</li>
-                    {{end}}
-                    </ul>
-                    {{end}}
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.MFA.IsActive}}data-i18n="status.active"{{else}}data-i18n="status.disabled"{{end}}></p>
+                {{- if .Status.MFA.IsActive}}
+                {{range .Status.MFA.TOTPConfigs}}
+                <div class="d-flex flex-column">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="general.configuration"></span> "{{.Name}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="general.issuer"></span> "{{.Issuer}}"
+                    </p>
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="status.algorithm"></span> "{{.Algo}}"
+                    </p>
+                </div>
+                {{- end}}
+                {{- end}}
             </div>
         </div>
 
-        <div class="card mb-2 {{ if .Status.DataProvider.IsActive}}border-left-success{{else}}border-left-warning{{end}}">
+        <div class="card mt-10">
+            <div class="card-header bg-light">
+                <h3 data-i18n="general.data_provider" class="card-title section-title-inner">Database</h3>
+            </div>
             <div class="card-body">
-                <h6 class="card-title font-weight-bold">Data provider</h6>
-                <p class="card-text">
-                    Status: {{ if .Status.DataProvider.IsActive}}"OK"{{else}}"{{.Status.DataProvider.Error}}"{{end}}
-                    <br>
-                    Driver: "{{.Status.DataProvider.Driver}}"
-                </p>
+                <p class="fs-3 fw-semibold mb-4" {{if .Status.DataProvider.IsActive}}data-i18n="status.active"{{else}}{{.Status.DataProvider.Error}}{{end}}></p>
+                <div class="d-flex flex-column">
+                    <p class="fs-5 fw-semibold">
+                        <span class="text-success" data-i18n="general.driver"></span> "{{.Status.DataProvider.Driver}}"
+                    </p>
+                </div>
             </div>
         </div>
 
     </div>
 </div>
-
-{{end}}
+{{- end}}