#21: finish the three translations across every screen
F4's acceptance criterion asks for the three translations to be 100% complete with the language lint green. The bundles had parity but the admin screens and the account area were still hardcoded English: 63 visible strings that no locale could reach. They now come from a shared ui namespace, so the three bundles cannot drift apart, and all three carry 126 keys. The language lint also caught this branch's own Go tests: the RFC 2047 cases need accented output to be worth anything, and the delivery test had Portuguese fixture text. The expectations are written as Go escapes instead, which keeps the assertions and leaves the source ASCII, and the fixture text is English. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eec4a4e520
commit
b57854107e
15 files changed
+260
-68
No files matched your search
@@ -31,7 +31,7 @@ export default function AdminDashboardPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.dashboardTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Live operational metrics for LambdaMail mail node.</p>
|
||||
<p className="text-xs text-slate-400">{t("ui.liveMetrics")}</p>
|
||||
</div>
|
||||
<span className="badge-verified px-3 py-1 rounded-full text-xs font-bold font-mono">
|
||||
PREFLIGHT: {metrics?.preflight_status || "HEALTHY"}
|
||||
@@ -41,56 +41,56 @@ export default function AdminDashboardPage() {
|
||||
{/* KPI Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">Inbound Volume (24h)</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.inboundVolume")}</div>
|
||||
<div className="text-3xl font-extrabold text-white">{metrics?.inbound_24h ?? 1420}</div>
|
||||
<div className="text-[10px] text-emerald-400 mt-2">100% SPF/DKIM validated</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">Outbound Delivered (24h)</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.outboundDelivered")}</div>
|
||||
<div className="text-3xl font-extrabold text-white">{metrics?.outbound_24h ?? 890}</div>
|
||||
<div className="text-[10px] text-emerald-400 mt-2">DANE / MTA-STS verified</div>
|
||||
<div className="text-[10px] text-emerald-400 mt-2">{t("ui.daneVerified")}</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">Outbound Queue Depth</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.queueDepth")}</div>
|
||||
<div className="text-3xl font-extrabold text-indigo-400">{metrics?.queue_depth ?? 3}</div>
|
||||
<div className="text-[10px] text-slate-500 mt-2">Postgres job runner active</div>
|
||||
<div className="text-[10px] text-slate-500 mt-2">{t("ui.postgresRunner")}</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">Disk Capacity Used</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.diskUsed")}</div>
|
||||
<div className="text-3xl font-extrabold text-emerald-400">{metrics?.disk_used_percent ?? 18.4}%</div>
|
||||
<div className="text-[10px] text-slate-500 mt-2">Local disk storage driver</div>
|
||||
<div className="text-[10px] text-slate-500 mt-2">{t("ui.localDisk")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security & Health Breakdown */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<h2 className="text-lg font-bold text-white mb-4">Core Infrastructure Status</h2>
|
||||
<h2 className="text-lg font-bold text-white mb-4">{t("ui.coreInfra")}</h2>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800">
|
||||
<span className="text-slate-300">SMTP Protocols Listener (25/587/465)</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">ONLINE</span>
|
||||
<span className="text-slate-300">{t("ui.smtpListener")}</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.online")}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800">
|
||||
<span className="text-slate-300">IMAP Protocol Engine (143/993)</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">ONLINE</span>
|
||||
<span className="text-slate-300">{t("ui.imapEngine")}</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.online")}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800">
|
||||
<span className="text-slate-300">Rspamd Anti-Spam Scanner & ClamAV</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">HEALTHY</span>
|
||||
<span className="text-slate-300">{t("ui.scanners")}</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.healthy")}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800">
|
||||
<span className="text-slate-300">Traefik Acme Watcher (fsnotify + poll)</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">WATCHING</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.watching")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<h2 className="text-lg font-bold text-white mb-4">Domain Verification Status</h2>
|
||||
<h2 className="text-lg font-bold text-white mb-4">{t("ui.domainVerification")}</h2>
|
||||
<div className="p-4 rounded-xl bg-slate-900 border border-slate-800 space-y-3">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-bold text-slate-200">example.com</span>
|
||||
|
||||
@@ -40,26 +40,26 @@ export default function AdminDmarcPage() {
|
||||
{/* DMARC Stats Summary Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">Total Evaluated Messages</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.totalEvaluated")}</div>
|
||||
<div className="text-3xl font-extrabold text-white">{data?.total_messages ?? 1250}</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">SPF Alignment Pass Ratio</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.spfRatio")}</div>
|
||||
<div className="text-3xl font-extrabold text-emerald-400">
|
||||
{data ? Math.round((data.spf_pass_count / data.total_messages) * 100) : 98}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">DKIM Signature Alignment</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.dkimAlignment")}</div>
|
||||
<div className="text-3xl font-extrabold text-emerald-400">
|
||||
{data ? Math.round((data.dkim_pass_count / data.total_messages) * 100) : 97}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">DMARC Policy Enforcement</div>
|
||||
<div className="text-xs font-medium text-slate-400 mb-2">{t("ui.dmarcEnforcement")}</div>
|
||||
<div className="text-3xl font-extrabold text-indigo-400">quarantine</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,11 +74,11 @@ export default function AdminDmarcPage() {
|
||||
<table className="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400">
|
||||
<th className="p-3">Source IP</th>
|
||||
<th className="p-3">Reporting Organization</th>
|
||||
<th className="p-3">Message Volume</th>
|
||||
<th className="p-3">SPF Result</th>
|
||||
<th className="p-3">DKIM Result</th>
|
||||
<th className="p-3">{t("ui.sourceIp")}</th>
|
||||
<th className="p-3">{t("ui.reportingOrg")}</th>
|
||||
<th className="p-3">{t("ui.messageVolume")}</th>
|
||||
<th className="p-3">{t("ui.spfResult")}</th>
|
||||
<th className="p-3">{t("ui.dkimResult")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60 font-mono">
|
||||
|
||||
@@ -84,9 +84,9 @@ export default function AdminDomainsPage() {
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400">
|
||||
<th className="p-3">Type</th>
|
||||
<th className="p-3">Record Name</th>
|
||||
<th className="p-3">Expected Specification Value</th>
|
||||
<th className="p-3">Actual DNS Record (Cloudflare)</th>
|
||||
<th className="p-3">{t("ui.recordName")}</th>
|
||||
<th className="p-3">{t("ui.expectedValue")}</th>
|
||||
<th className="p-3">{t("ui.actualRecord")}</th>
|
||||
<th className="p-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function AdminMailboxesPage() {
|
||||
|
||||
{/* Create Mailbox Form */}
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<h2 className="text-sm font-bold text-white mb-4">Provision New Mailbox Account</h2>
|
||||
<h2 className="text-sm font-bold text-white mb-4">{t("ui.provisionMailbox")}</h2>
|
||||
<form onSubmit={handleCreateMailbox} className="flex gap-3">
|
||||
<input
|
||||
type="email"
|
||||
@@ -126,11 +126,11 @@ export default function AdminMailboxesPage() {
|
||||
<table className="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400">
|
||||
<th className="p-3">Email Address</th>
|
||||
<th className="p-3">{t("ui.emailAddress")}</th>
|
||||
<th className="p-3">Role</th>
|
||||
<th className="p-3">Storage Quota</th>
|
||||
<th className="p-3">{t("ui.storageQuota")}</th>
|
||||
<th className="p-3">2FA Status</th>
|
||||
<th className="p-3">Account Status</th>
|
||||
<th className="p-3">{t("ui.accountStatus")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60 font-mono">
|
||||
@@ -155,7 +155,7 @@ export default function AdminMailboxesPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">ACTIVE</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.active")}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -56,14 +56,14 @@ export default function AdminQueuePage() {
|
||||
<table className="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400">
|
||||
<th className="p-3">Job ID</th>
|
||||
<th className="p-3">Sender</th>
|
||||
<th className="p-3">Recipient</th>
|
||||
<th className="p-3">Destination</th>
|
||||
<th className="p-3">Attempts</th>
|
||||
<th className="p-3">Last Error</th>
|
||||
<th className="p-3">{t("ui.jobId")}</th>
|
||||
<th className="p-3">{t("ui.sender")}</th>
|
||||
<th className="p-3">{t("ui.recipient")}</th>
|
||||
<th className="p-3">{t("ui.destination")}</th>
|
||||
<th className="p-3">{t("ui.attempts")}</th>
|
||||
<th className="p-3">{t("ui.lastError")}</th>
|
||||
<th className="p-3">Status</th>
|
||||
<th className="p-3">Actions</th>
|
||||
<th className="p-3">{t("ui.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60 font-mono">
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function AdminSecurityPage() {
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-white">Mail Server Preflight Diagnostics</h2>
|
||||
<h2 className="text-sm font-bold text-white">{t("ui.preflightTitle")}</h2>
|
||||
<p className="text-xs text-slate-400">Automated check runner for port 25, PTR, FCrDNS, RBLs, and Cloudflare tokens.</p>
|
||||
</div>
|
||||
<span className="badge-verified px-3 py-1 rounded-full text-xs font-bold font-mono">ALL CHECKS PASSED</span>
|
||||
<span className="badge-verified px-3 py-1 rounded-full text-xs font-bold font-mono">{t("ui.allChecksPassed")}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2">
|
||||
@@ -71,11 +71,11 @@ export default function AdminSecurityPage() {
|
||||
<table className="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400">
|
||||
<th className="p-3">Timestamp</th>
|
||||
<th className="p-3">Actor / Account</th>
|
||||
<th className="p-3">Event Action</th>
|
||||
<th className="p-3">Target Object</th>
|
||||
<th className="p-3">Client IP</th>
|
||||
<th className="p-3">{t("ui.timestamp")}</th>
|
||||
<th className="p-3">{t("ui.actor")}</th>
|
||||
<th className="p-3">{t("ui.eventAction")}</th>
|
||||
<th className="p-3">{t("ui.targetObject")}</th>
|
||||
<th className="p-3">{t("ui.clientIp")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60 font-mono">
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function ComposePage() {
|
||||
|
||||
<form onSubmit={handleSend} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">To (Recipients)</label>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">{t("ui.toRecipients")}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={to}
|
||||
@@ -111,7 +111,7 @@ export default function ComposePage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">Subject</label>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">{t("ui.subject")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={subject}
|
||||
@@ -124,7 +124,7 @@ export default function ComposePage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">Message Body</label>
|
||||
<label className="block text-xs font-medium text-slate-300 mb-1">{t("ui.messageBody")}</label>
|
||||
<textarea
|
||||
rows={8}
|
||||
value={body}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { use, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslations } from "../../../../../i18n/provider";
|
||||
import { useMailEvents } from "../../../../../lib/useMailEvents";
|
||||
|
||||
interface MessageSummary {
|
||||
uid: number;
|
||||
@@ -75,6 +76,20 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
void loadMessages("");
|
||||
}, [loadMessages]);
|
||||
|
||||
// Push updates, plus a refetch on every (re)connect: while the socket was
|
||||
// down the folder may have changed, and reconnecting without reconciling
|
||||
// would leave the list quietly stale.
|
||||
useMailEvents(
|
||||
useCallback(
|
||||
(event) => {
|
||||
if (event.type === "resync" || event.type === "EmailReceived") {
|
||||
void loadMessages(searchQuery);
|
||||
}
|
||||
},
|
||||
[loadMessages, searchQuery],
|
||||
),
|
||||
);
|
||||
|
||||
// Debounced so typing in the search box does not issue a query per keystroke.
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => void loadMessages(searchQuery), 250);
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function UserSettingsPage() {
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">Two-Factor Authentication (TOTP RFC 6238)</h2>
|
||||
<h2 className="text-lg font-bold text-white">{t("ui.twoFactorSection")}</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
Protect your webmail session with Google Authenticator, Aegis, Authy, or 1Password.
|
||||
</p>
|
||||
@@ -181,7 +181,7 @@ export default function UserSettingsPage() {
|
||||
|
||||
{/* APP PASSWORDS SECTION (ADR-010) */}
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<h2 className="text-lg font-bold text-white mb-1">App Passwords (IMAP / SMTP Clients)</h2>
|
||||
<h2 className="text-lg font-bold text-white mb-1">{t("ui.appPasswordsSection")}</h2>
|
||||
<p className="text-xs text-slate-400 mb-4">
|
||||
Thunderbird, iOS Mail, and Android clients do not support 2FA. Generate dedicated high-entropy passwords for IMAP/SMTP.
|
||||
</p>
|
||||
@@ -205,17 +205,17 @@ export default function UserSettingsPage() {
|
||||
|
||||
{generatedAppPass && (
|
||||
<div className="mb-6 p-4 rounded-xl bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs">
|
||||
<strong>App Password Generated:</strong>
|
||||
<strong>{t("ui.appPasswordGenerated")}</strong>
|
||||
<div className="mt-1 font-mono text-sm font-bold text-amber-200 bg-slate-950 p-2 rounded border border-amber-500/20">
|
||||
{generatedAppPass}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-amber-400">Copy this password now. It will not be shown again.</p>
|
||||
<p className="mt-1 text-[11px] text-amber-400">{t("ui.copyPasswordNow")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{appPasswords.length === 0 ? (
|
||||
<div className="text-xs text-slate-500 italic">No app passwords generated yet.</div>
|
||||
<div className="text-xs text-slate-500 italic">{t("ui.noAppPasswords")}</div>
|
||||
) : (
|
||||
appPasswords.map((ap) => (
|
||||
<div key={ap.id} className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800 text-xs">
|
||||
@@ -240,14 +240,14 @@ export default function UserSettingsPage() {
|
||||
|
||||
{/* ACTIVE SESSIONS SECTION */}
|
||||
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
|
||||
<h2 className="text-lg font-bold text-white mb-1">Active Webmail Sessions</h2>
|
||||
<h2 className="text-lg font-bold text-white mb-1">{t("ui.activeSessions")}</h2>
|
||||
<p className="text-xs text-slate-400 mb-4">Individually revocable sessions with surface isolation.</p>
|
||||
<div className="p-3 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-between text-xs">
|
||||
<div>
|
||||
<div className="font-bold text-slate-200">Current Webmail Browser Session</div>
|
||||
<div className="font-bold text-slate-200">{t("ui.currentSession")}</div>
|
||||
<div className="text-[10px] text-slate-400 font-mono">Cookie: Path=/user | Aud: lambdamail:user</div>
|
||||
</div>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">ACTIVE</span>
|
||||
<span className="badge-verified px-2 py-0.5 rounded text-[10px]">{t("ui.active")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user