#21: put real mail on the webmail screens
The mail list rendered two hardcoded messages and the reader showed their canned HTML; compose ran a countdown and then navigated away without ever submitting anything. Both now use the API, including search, the unread badges in the sidebar (which were the literals "3" and "1", so an empty inbox still advertised unread mail), and marking a message read on open. The undo window now means what it says: the message is held in the browser and only submitted when the countdown elapses, so cancelling it means nothing was ever queued rather than trying to recall it afterwards. Message bodies render in an iframe with an empty sandbox - no scripts, no same-origin - and remote references are stripped until the reader asks for images, so a tracking pixel cannot fire on open. The previous sandbox allowed popups. The admin page titles and the language switcher labels now come from the bundles. The labels live there rather than in the source because PLAN.md R9 keeps non-ASCII out of code, and each language should be named the way that language names it. ADR-001 assigned the mail API to the TypeScript storage service; the reasoning for putting it in Go instead is recorded in PLAN.md, which .gitignore deliberately keeps out of the repository, so that revision lives only in the working copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48fe3c68f3
commit
3e60852551
15 files changed
+299
-166
No files matched your search
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface DashboardMetrics {
|
||||
inbound_24h: number;
|
||||
@@ -15,6 +16,7 @@ interface DashboardMetrics {
|
||||
}
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
const t = useTranslations();
|
||||
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,7 +30,7 @@ export default function AdminDashboardPage() {
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">System Overview & Performance</h1>
|
||||
<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>
|
||||
</div>
|
||||
<span className="badge-verified px-3 py-1 rounded-full text-xs font-bold font-mono">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface DmarcSource {
|
||||
ip: string;
|
||||
@@ -19,6 +20,7 @@ interface DmarcData {
|
||||
}
|
||||
|
||||
export default function AdminDmarcPage() {
|
||||
const t = useTranslations();
|
||||
const [data, setData] = useState<DmarcData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,7 +33,7 @@ export default function AdminDmarcPage() {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">DMARC Aggregate Reports Analytics</h1>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.dmarcTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Ingested XML aggregate reports breakdown (dmarc_reports table).</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface DnsRecord {
|
||||
type: string;
|
||||
@@ -27,6 +28,7 @@ const DNS_RECORDS_13: DnsRecord[] = [
|
||||
];
|
||||
|
||||
export default function AdminDomainsPage() {
|
||||
const t = useTranslations();
|
||||
const [reconciling, setReconciling] = useState(false);
|
||||
const [syncMessage, setSyncMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -43,7 +45,7 @@ export default function AdminDomainsPage() {
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">Domain & 13-DNS Records Automation</h1>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.domainsTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Live DNS diff reconciliation against Cloudflare API (PLAN.md Section 16 F3/F4 requirement).</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface Mailbox {
|
||||
id: string;
|
||||
@@ -19,6 +20,7 @@ const INITIAL_MAILBOXES: Mailbox[] = [
|
||||
];
|
||||
|
||||
export default function AdminMailboxesPage() {
|
||||
const t = useTranslations();
|
||||
const [mailboxes, setMailboxes] = useState<Mailbox[]>(INITIAL_MAILBOXES);
|
||||
const [mfaPolicy, setMfaPolicy] = useState<"optional" | "required_admins" | "required_all">("required_admins");
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
@@ -44,7 +46,7 @@ export default function AdminMailboxesPage() {
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">Mailboxes & Account Management</h1>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.mailboxesTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Configure mailboxes, quota allocation, and domain MFA policy.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface QueueJob {
|
||||
id: string;
|
||||
@@ -18,6 +19,7 @@ const INITIAL_JOBS: QueueJob[] = [
|
||||
];
|
||||
|
||||
export default function AdminQueuePage() {
|
||||
const t = useTranslations();
|
||||
const [jobs, setJobs] = useState<QueueJob[]>(INITIAL_JOBS);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -34,7 +36,7 @@ export default function AdminQueuePage() {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">Outbound Delivery Queue Control</h1>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.queueTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Inspect outbound queue, retry retriable jobs, or freeze problematic destinations.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslations } from "../../../../i18n/provider";
|
||||
|
||||
interface AuditEntry {
|
||||
id: string;
|
||||
@@ -23,6 +24,7 @@ const MOCK_AUDIT_LOGS: AuditEntry[] = [
|
||||
];
|
||||
|
||||
export default function AdminSecurityPage() {
|
||||
const t = useTranslations();
|
||||
const [preflightChecks, setPreflightChecks] = useState<PreflightCheck[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,7 +37,7 @@ export default function AdminSecurityPage() {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">Security, Audit Log & Preflight</h1>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">{t("admin.securityTitle")}</h1>
|
||||
<p className="text-xs text-slate-400">Immutable security event logging and mail server preflight diagnostics.</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user