diff --git a/services/webmail/messages/en.json b/services/webmail/messages/en.json index 60a82ef..0a4b87f 100644 --- a/services/webmail/messages/en.json +++ b/services/webmail/messages/en.json @@ -1,6 +1,7 @@ { "admin": { "addAlias": "Add alias", + "aliasTarget": "Destination address", "aliasesTitle": "Aliases", "auditLog": "Audit log", "backToWebmail": "Back to webmail", @@ -9,7 +10,12 @@ "checksPassed": "{passed} of {total} passed", "createAccount": "Create account", "createAlias": "Create a routing alias", + "csvImportFailed": "The CSV import failed", + "csvImported": "Imported {count} mailboxes.", + "csvIntro": "Paste CSV to provision several accounts with their RFC 6154 folders. Columns: email, role, quota_mb.", "csvPreview": "Preview", + "csvRun": "Run import", + "csvTitle": "Bulk mailbox import (CSV)", "dashboardSubtitle": "Live figures read from the database.", "dashboardTitle": "Overview", "dkimAlgorithm": "Key type", @@ -34,6 +40,7 @@ "jobRetried": "Job {id} was queued for another attempt.", "lastChecked": "Last checked: {when}", "lastReload": "Last reload", + "lockAccount": "Lock", "mailHost": "Mail host", "mailboxCount": "{count} mailboxes", "mailboxesSubtitle": "Accounts, aliases, CSV import and the second-factor policy.", @@ -55,6 +62,7 @@ "queueEmpty": "The outbound queue is empty.", "queueSubtitle": "Inspect the outbound queue, retry a job, or cancel one.", "queueTitle": "Outbound queue", + "quotaMb": "Quota (MB)", "readinessIntro": "Checks the server reports about itself before it carries production mail.", "reconcileDns": "Reconcile DNS", "reconcileNotImplemented": "Per-record DNS verification is not implemented yet: this records the request and re-reads the stored status, it does not query a resolver.", @@ -65,7 +73,10 @@ "saveThresholds": "Save thresholds", "securitySubtitle": "Readiness checks, TLS state, spam thresholds and the audit trail.", "securityTitle": "Security", + "tabAccounts": "Accounts", + "tabAliases": "Aliases", "tabAudit": "Audit trail", + "tabCsv": "Bulk import", "tabReadiness": "Readiness", "tabRspamd": "Spam thresholds", "tabTls": "TLS", @@ -73,8 +84,11 @@ "tlsTitle": "Certificate and transport", "traceNoResults": "No trace found for that queue ID.", "tracePlaceholder": "Queue ID", + "traceResultFor": "Trace for {id}", + "traceStep": "Step {n}", "traceTitle": "Trace a message", "unknownSni": "Unknown SNI requests", + "unlockAccount": "Unlock", "verifyIntro": "Resolves every expected record against public resolvers and reports each one.", "watcherHealthy": "Certificate watcher" }, @@ -236,6 +250,7 @@ "valuePlaceholder": "Value to match..." }, "ui": { + "accountLocked": "Locked", "accountStatus": "Account status", "actions": "Actions", "active": "Active", @@ -275,6 +290,8 @@ "mailboxesActive": "Active mailboxes", "messageBody": "Message", "messageVolume": "Messages", + "mfaActive": "TOTP active", + "mfaNone": "No second factor", "newPassword": "New password", "noAppPasswords": "No app passwords yet.", "noData": "No data yet", @@ -288,6 +305,7 @@ "recipient": "Recipient", "recordName": "Record", "reportingOrg": "Reporting organisation", + "role": "Role", "scanners": "Spam and virus scanning", "sender": "Sender", "signOutOthers": "Sign out other devices", diff --git a/services/webmail/messages/es.json b/services/webmail/messages/es.json index a4bd6dc..14b2071 100644 --- a/services/webmail/messages/es.json +++ b/services/webmail/messages/es.json @@ -1,6 +1,7 @@ { "admin": { "addAlias": "Añadir apelido", + "aliasTarget": "Dirección de destino", "aliasesTitle": "Alias", "auditLog": "Registro de auditoría", "backToWebmail": "Volver al webmail", @@ -9,7 +10,12 @@ "checksPassed": "{passed} de {total} aprobadas", "createAccount": "Crear cuenta", "createAlias": "Crear un alias de enrutamiento", + "csvImportFailed": "La importación del CSV falló", + "csvImported": "{count} buzones importados.", + "csvIntro": "Pega el CSV para crear varias cuentas con sus carpetas de la RFC 6154. Columnas: email, rol, cuota_mb.", "csvPreview": "Vista previa", + "csvRun": "Ejecutar la importación", + "csvTitle": "Importar buzones en masa (CSV)", "dashboardSubtitle": "Cifras en vivo leídas de la base de datos.", "dashboardTitle": "Resumen", "dkimAlgorithm": "Tipo de clave", @@ -34,6 +40,7 @@ "jobRetried": "El trabajo {id} se volvió a poner en cola para otro intento.", "lastChecked": "Última comprobación: {when}", "lastReload": "Última recarga", + "lockAccount": "Bloquear", "mailHost": "Host de correo", "mailboxCount": "{count} buzones", "mailboxesSubtitle": "Cuentas, alias, importación CSV y la política de segundo factor.", @@ -55,6 +62,7 @@ "queueEmpty": "La cola de salida está vacía.", "queueSubtitle": "Inspeccione la cola de salida, reintente un trabajo o cancele uno.", "queueTitle": "Cola de salida", + "quotaMb": "Cuota (MB)", "readinessIntro": "Verificaciones que el servidor informa sobre sí mismo antes de transportar correo en producción.", "reconcileDns": "Reconciliar DNS", "reconcileNotImplemented": "La verificación de registros DNS individuales aún no está implementada: esto registra la solicitud y relee el estado almacenado, no consulta un resolutor.", @@ -65,7 +73,10 @@ "saveThresholds": "Guardar umbrales", "securitySubtitle": "Verificaciones de preparación, estado TLS, umbrales de spam y el registro de auditoría.", "securityTitle": "Seguridad", + "tabAccounts": "Cuentas", + "tabAliases": "Alias", "tabAudit": "Registro de auditoría", + "tabCsv": "Importación masiva", "tabReadiness": "Preparación", "tabRspamd": "Umbrales de spam", "tabTls": "TLS", @@ -73,8 +84,11 @@ "tlsTitle": "Certificado y transporte", "traceNoResults": "No se encontró rastro para ese ID de cola.", "tracePlaceholder": "ID de la cola", + "traceResultFor": "Rastreo de {id}", + "traceStep": "Paso {n}", "traceTitle": "Rastrear un mensaje", "unknownSni": "Solicitudes SNI desconocidas", + "unlockAccount": "Desbloquear", "verifyIntro": "Resuelve cada registro esperado en resolutores públicos e informa uno por uno.", "watcherHealthy": "Monitor de certificado" }, @@ -236,6 +250,7 @@ "valuePlaceholder": "Valor a coincidir..." }, "ui": { + "accountLocked": "Bloqueada", "accountStatus": "Estado de la cuenta", "actions": "Acciones", "active": "Activo", @@ -275,6 +290,8 @@ "mailboxesActive": "Buzones activos", "messageBody": "Mensaje", "messageVolume": "Mensajes", + "mfaActive": "TOTP activo", + "mfaNone": "Sin segundo factor", "newPassword": "Nueva contraseña", "noAppPasswords": "Aún no hay contraseñas de aplicación.", "noData": "Aún sin datos", @@ -288,6 +305,7 @@ "recipient": "Destinatario", "recordName": "Registro", "reportingOrg": "Organización informante", + "role": "Rol", "scanners": "Análisis de spam y virus", "sender": "Remitente", "signOutOthers": "Cerrar sesión en otros dispositivos", diff --git a/services/webmail/messages/pt-BR.json b/services/webmail/messages/pt-BR.json index 1ae1bd0..a73ca10 100644 --- a/services/webmail/messages/pt-BR.json +++ b/services/webmail/messages/pt-BR.json @@ -1,6 +1,7 @@ { "admin": { "addAlias": "Adicionar apelido", + "aliasTarget": "Endereço de destino", "aliasesTitle": "Apelidos", "auditLog": "Trilha de auditoria", "backToWebmail": "Voltar ao webmail", @@ -9,7 +10,12 @@ "checksPassed": "{passed} de {total} aprovadas", "createAccount": "Criar conta", "createAlias": "Criar um apelido de roteamento", + "csvImportFailed": "A importação do CSV falhou", + "csvImported": "{count} caixas importadas.", + "csvIntro": "Cole o CSV para criar várias contas com as pastas da RFC 6154. Colunas: email, papel, cota_mb.", "csvPreview": "Pré-visualizar", + "csvRun": "Executar importação", + "csvTitle": "Importar caixas em massa (CSV)", "dashboardSubtitle": "Números ao vivo lidos do banco de dados.", "dashboardTitle": "Visão geral", "dkimAlgorithm": "Tipo de chave", @@ -34,6 +40,7 @@ "jobRetried": "O trabalho {id} foi recolocado na fila para nova tentativa.", "lastChecked": "Última verificação: {when}", "lastReload": "Última recarga", + "lockAccount": "Bloquear", "mailHost": "Host de e-mail", "mailboxCount": "{count} caixas de e-mail", "mailboxesSubtitle": "Contas, apelidos, importação CSV e a política de segundo fator.", @@ -55,6 +62,7 @@ "queueEmpty": "A fila de saída está vazia.", "queueSubtitle": "Inspecione a fila de saída, tente reenviar um trabalho ou cancele um.", "queueTitle": "Fila de saída", + "quotaMb": "Cota (MB)", "readinessIntro": "Verificações que o servidor relata sobre si mesmo antes de transportar mensagens em produção.", "reconcileDns": "Reconciliar DNS", "reconcileNotImplemented": "A verificação de registros DNS individuais ainda não foi implementada: isto registra a solicitação e relê o status armazenado, não consulta um resolvedor.", @@ -65,7 +73,10 @@ "saveThresholds": "Salvar limites", "securitySubtitle": "Verificações de prontidão, estado do TLS, limites de spam e a trilha de auditoria.", "securityTitle": "Segurança", + "tabAccounts": "Contas", + "tabAliases": "Apelidos", "tabAudit": "Trilha de auditoria", + "tabCsv": "Importação em massa", "tabReadiness": "Prontidão", "tabRspamd": "Limites de spam", "tabTls": "TLS", @@ -73,8 +84,11 @@ "tlsTitle": "Certificado e transporte", "traceNoResults": "Nenhum rastro encontrado para esse ID de fila.", "tracePlaceholder": "ID da fila", + "traceResultFor": "Rastreamento de {id}", + "traceStep": "Etapa {n}", "traceTitle": "Rastrear uma mensagem", "unknownSni": "Requisições SNI desconhecidas", + "unlockAccount": "Desbloquear", "verifyIntro": "Resolve cada registro esperado em servidores DNS públicos e relata um a um.", "watcherHealthy": "Monitor de certificado" }, @@ -236,6 +250,7 @@ "valuePlaceholder": "Valor a corresponder..." }, "ui": { + "accountLocked": "Bloqueada", "accountStatus": "Status da conta", "actions": "Ações", "active": "Ativo", @@ -275,6 +290,8 @@ "mailboxesActive": "Caixas ativas", "messageBody": "Mensagem", "messageVolume": "Mensagens", + "mfaActive": "TOTP ativo", + "mfaNone": "Sem segundo fator", "newPassword": "Nova senha", "noAppPasswords": "Nenhuma senha de aplicativo ainda.", "noData": "Ainda sem dados", @@ -288,6 +305,7 @@ "recipient": "Destinatário", "recordName": "Registro", "reportingOrg": "Organização que reportou", + "role": "Papel", "scanners": "Análise de spam e vírus", "sender": "Remetente", "signOutOthers": "Sair dos outros dispositivos", diff --git a/services/webmail/package-lock.json b/services/webmail/package-lock.json index 73087ea..498a804 100644 --- a/services/webmail/package-lock.json +++ b/services/webmail/package-lock.json @@ -2921,9 +2921,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", diff --git a/services/webmail/package.json b/services/webmail/package.json index c046885..0bece89 100644 --- a/services/webmail/package.json +++ b/services/webmail/package.json @@ -35,6 +35,7 @@ "sharp": "^0.35.0", "next": { "postcss": "^8.5.25" - } + }, + "nanoid": "^3.3.18" } } diff --git a/services/webmail/src/app/(admin)/admin/dashboard/page.tsx b/services/webmail/src/app/(admin)/admin/dashboard/page.tsx index c15f8dc..9b6a786 100644 --- a/services/webmail/src/app/(admin)/admin/dashboard/page.tsx +++ b/services/webmail/src/app/(admin)/admin/dashboard/page.tsx @@ -7,7 +7,6 @@ import { Send, Layers, HardDrive, - CheckCircle2, Server, ShieldCheck, Globe, @@ -15,7 +14,6 @@ import { Zap, } from "lucide-react"; import { useTranslations } from "../../../../i18n/provider"; -import { Card, CardHeader, CardTitle } from "../../../../components/ui/Card"; import { Badge } from "../../../../components/ui/Badge"; import { Button } from "../../../../components/ui/Button"; @@ -70,30 +68,27 @@ export default function AdminDashboardPage() { const shown = (value: number | undefined, format?: (n: number) => string) => value === undefined || value === null ? t("ui.noData") : format ? format(value) : String(value); + /* The KPI tiles no longer each carry their own hue. Four colours across four + numbers implied four kinds of thing; they are all just counts, and the one + accent marks the icon. */ const kpis = [ { title: t("ui.inboundVolume"), value: shown(metrics?.inbound_24h), subtext: t("ui.messageVolume"), icon: Inbox, - color: "text-indigo-400", - bg: "bg-indigo-500/10", }, { title: t("ui.outboundDelivered"), value: shown(metrics?.outbound_24h), subtext: t("ui.daneVerified"), icon: Send, - color: "text-emerald-400", - bg: "bg-emerald-500/10", }, { title: t("ui.queueDepth"), value: shown(metrics?.queue_depth), subtext: t("ui.postgresRunner"), icon: Layers, - color: "text-cyan-400", - bg: "bg-cyan-500/10", }, { title: t("ui.diskUsed"), @@ -102,42 +97,42 @@ export default function AdminDashboardPage() { ? `${t("ui.storageQuota")}: ${formatBytes(metrics.storage_quota_bytes)}` : t("ui.localDisk"), icon: HardDrive, - color: "text-amber-400", - bg: "bg-amber-500/10", }, ]; + const panel = "flex flex-col gap-3.5 rounded-2xl bg-dark-panel p-[18px] shadow-edge"; + return ( -
{t("admin.dashboardSubtitle")}
+{t("admin.dashboardSubtitle")}
{t("admin.dmarcSubtitle")}
+{t("admin.dmarcSubtitle")}
| {t("ui.sourceIp")} | -{t("ui.reportingOrg")} | -{t("ui.messageVolume")} | -{t("ui.spfResult")} | -{t("ui.dkimResult")} | +|
|---|---|---|---|---|---|
| {t("ui.sourceIp")} | +{t("ui.reportingOrg")} | +{t("ui.messageVolume")} | +{t("ui.spfResult")} | +{t("ui.dkimResult")} | |
| {src.ip} | -{src.org} | -{src.count} | -- - {src.spf.toUpperCase()} - + | ||
| {src.ip} | +{src.org} | +{src.count} | +
+ {/* These were "badge-verified" and "badge-danger", class
+ names no stylesheet defines, so both verdicts rendered
+ as identical unstyled text. */}
+ |
- - - {src.dkim.toUpperCase()} - + |
+ |
{t("admin.verifyIntro")}
+{t("admin.verifyIntro")}
-| {t("ui.recordName")} | -{t("ui.expectedValue")} | -{t("common.status")} | +||
|---|---|---|---|---|
| {t("ui.recordName")} | +{t("ui.expectedValue")} | +{t("common.status")} | ||
| - {rec.type} {rec.name} + | ||||
|
+ |
- {rec.expected} | -+ | + {rec.expected} + | +
{rec.verified ? (
-
-
-
-
- {t("admin.onboardTitle")}-{t("admin.onboardIntro")} +{t("admin.onboardTitle")}+{t("admin.onboardIntro")}
-
-
- |