+
+
+
Email Server
+
Manage email domains, accounts, and mail services
+
+
+ {!isInstalled ? (
+
+ ) : (
+ <>
+
+ >
+ )}
+
+
+
+ {!isInstalled ? (
+
+
email
+
No Email Server Installed
+
Install an email server to manage domains, accounts, and mail delivery.
+
+
+ ) : (
+ <>
+
+
+
+ {status?.postfix?.running ? 'check_circle' : 'pause_circle'}
+
+
+ Postfix
+ {status?.postfix?.running ? 'Running' : 'Stopped'}
+
+
+
+
+ {status?.dovecot?.running ? 'check_circle' : 'pause_circle'}
+
+
+ Dovecot
+ {status?.dovecot?.running ? 'Running' : 'Stopped'}
+
+
+
+
+ domain
+
+
+ Domains
+ {totalDomains}
+
+
+
+
+ people
+
+
+ Accounts
+ {totalAccounts}
+
+
+
+
+
+ {['overview', 'domains', 'accounts', 'aliases', 'dns', 'spam', 'webmail'].map((tab) => (
+
+ ))}
+
+
+
+ {/* ===== OVERVIEW TAB ===== */}
+ {activeTab === 'overview' && (
+
+
Service Status
+
+ {services.map((svc) => (
+
+
+
{svc.label}
+
+ {svc.data?.installed ? 'Installed' : 'Not Installed'}
+
+
+
+
+ Status:
+
+ {svc.data?.running ? 'Running' : 'Stopped'}
+
+
+ {svc.data?.version && (
+
+ Version:
+ {svc.data.version}
+
+ )}
+ {svc.data?.installed && (
+
+ {svc.data?.running ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+ )}
+
+
+ ))}
+
+
+
Statistics
+
+
+
+ Total Domains
+ {totalDomains}
+
+
+
+
+ Total Accounts
+ {totalAccounts}
+
+
+
+
+ )}
+
+ {/* ===== DOMAINS TAB ===== */}
+ {activeTab === 'domains' && (
+
+
+
Email Domains
+
+
+ {domains.length === 0 ? (
+
+
domain
+
No email domains configured
+
+
+ ) : (
+
+ {domains.map((domain) => (
+
+
+
{domain.domain || domain.name}
+
+
+
+ Accounts: {domain.accounts_count || 0}
+ Aliases: {domain.aliases_count || 0}
+
+
+
+ DKIM {domain.dkim_valid ? '\u2713' : '\u2717'}
+
+
+ SPF {domain.spf_valid ? '\u2713' : '\u2717'}
+
+
+ DMARC {domain.dmarc_valid ? '\u2713' : '\u2717'}
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* ===== ACCOUNTS TAB ===== */}
+ {activeTab === 'accounts' && (
+
+
+
Email Accounts
+
+
+
+
+
+ {!selectedDomainId ? (
+
+
domain
+
Select a domain to view accounts
+
+ ) : accounts.length === 0 ? (
+
+
person_add
+
No accounts for this domain
+
+
+ ) : (
+
+
+
+
+ | Email |
+ Quota Usage |
+ Status |
+ Created |
+ Actions |
+
+
+
+ {accounts.map((account) => {
+ const usedMB = account.quota_used || 0;
+ const totalMB = account.quota || 1024;
+ const pct = totalMB > 0 ? Math.min(100, Math.round((usedMB / totalMB) * 100)) : 0;
+ return (
+
+ |
+ {account.email || account.username}
+ |
+
+
+
+ 90 ? 'var(--color-danger, #ef4444)' : pct > 70 ? 'var(--color-warning, #f59e0b)' : 'var(--color-success, #22c55e)',
+ borderRadius: '4px',
+ transition: 'width 0.3s ease'
+ }} />
+
+
+ {usedMB} / {totalMB} MB ({pct}%)
+
+
+ |
+
+
+ {account.active !== false ? 'Active' : 'Inactive'}
+
+ |
+ {account.created_at ? new Date(account.created_at).toLocaleDateString() : '-'} |
+
+
+
+
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+ )}
+
+ {/* ===== ALIASES & FORWARDING TAB ===== */}
+ {activeTab === 'aliases' && (
+
+
+
+
+
+
+ {!selectedDomainId ? (
+
+
domain
+
Select a domain to manage aliases and forwarding
+
+ ) : (
+
+ {/* Aliases Section */}
+
+
+
Aliases
+
+
+
+ {aliases.length === 0 ? (
+
+
No aliases configured
+
+ ) : (
+
+ {aliases.map((alias) => (
+
+
+ {alias.source}
+ →
+ {alias.destination}
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Forwarding Section */}
+
+
+
Forwarding
+
+
+
+
+
+
+
+ {!selectedForwardingAccount ? (
+
+
Select an account to view forwarding rules
+
+ ) : forwarding.length === 0 ? (
+
+ ) : (
+
+ {forwarding.map((rule) => (
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+ )}
+
+ {/* ===== DNS & AUTHENTICATION TAB ===== */}
+ {activeTab === 'dns' && (
+
+ {/* DNS Providers */}
+
+
DNS Providers
+
+
+ {dnsProviders.length === 0 ? (
+
+
No DNS providers configured
+
+
+ ) : (
+
+ {dnsProviders.map((provider) => (
+
+
+
{provider.name}
+ {provider.type}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* DKIM Records */}
+
DKIM Records
+ {domains.length === 0 ? (
+
No domains configured
+ ) : (
+
+ {domains.map((domain) => (
+
+
+
{domain.domain || domain.name} - DKIM
+
+
+ {domain.dkim_record ? (
+
+
+ {domain.dkim_record}
+
+
+
+ ) : (
+
DKIM record not available
+ )}
+
+
+ ))}
+
+ )}
+
+ {/* SPF / DMARC Records */}
+
SPF & DMARC Records
+ {domains.length === 0 ? (
+
No domains configured
+ ) : (
+
+ {domains.map((domain) => (
+
+
+
{domain.domain || domain.name}
+
+
+
+
+
+
+ {domain.spf_record || `v=spf1 mx a ~all`}
+
+
+
+
+
+
+
+
+ {domain.dmarc_record || `v=DMARC1; p=quarantine; rua=mailto:dmarc@${domain.domain || domain.name}`}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* ===== SPAM FILTER TAB ===== */}
+ {activeTab === 'spam' && (
+
+
SpamAssassin Configuration
+ {status?.spamassassin?.installed && (
+
+
+ {status.spamassassin.installed ? 'Installed' : 'Not Installed'}
+
+
+ {status.spamassassin.running ? 'Running' : 'Stopped'}
+
+ {status.spamassassin.version && (
+ v{status.spamassassin.version}
+ )}
+
+ )}
+
+
+
+ )}
+
+ {/* ===== WEBMAIL TAB ===== */}
+ {activeTab === 'webmail' && (
+
+
Roundcube Webmail
+
+
+
+
Status
+
+
+
+
+ {webmailStatus?.installed ? 'Installed' : 'Not Installed'}
+
+ {webmailStatus?.installed && (
+
+ {webmailStatus?.running ? 'Running' : 'Stopped'}
+
+ )}
+ {webmailStatus?.port && (
+ Port: {webmailStatus.port}
+ )}
+
+
+ {!webmailStatus?.installed ? (
+
+ ) : (
+
+ {webmailStatus?.running ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+ {webmailStatus?.url && (
+
+ Open Roundcube
+
+ )}
+
+ )}
+
+
+
+ {webmailStatus?.installed && (
+
+
+
Configure Nginx Proxy
+
+
+
+
+
+ setWebmailProxyDomain(e.target.value)}
+ placeholder="mail.example.com"
+ style={{ flex: 1 }}
+ />
+
+
+
Set up Nginx reverse proxy for Roundcube on this domain
+
+
+
+ )}
+
+ {webmailStatus?.installed && webmailStatus?.port && (
+
+ )}
+
+ )}
+
+ >
+ )}
+
+ {/* ===== MODALS ===== */}
+
+ {/* Install Email Server Modal */}
+ {showInstallModal && (
+
setShowInstallModal(false)}>
+
e.stopPropagation()}>
+
+
Install Email Server
+
+
+
+
This will install and configure Postfix, Dovecot, OpenDKIM, and SpamAssassin.
+
+
+ setInstallHostname(e.target.value)}
+ placeholder="mail.example.com"
+ />
+ The fully qualified domain name for your mail server
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Add Domain Modal */}
+ {showAddDomainModal && (
+
setShowAddDomainModal(false)}>
+
e.stopPropagation()}>
+
+
Add Email Domain
+
+
+
+
+
+ setNewDomain({ ...newDomain, domain: e.target.value })}
+ placeholder="example.com"
+ />
+
+
+
+
+ Select a DNS provider for automatic DNS record management
+
+ {newDomain.dns_provider_id && (
+
+
+ {zonesLoading ? (
+
Loading zones...
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+ )}
+
+ {/* Create Account Modal */}
+ {showCreateAccountModal && (
+
setShowCreateAccountModal(false)}>
+
e.stopPropagation()}>
+
+
Create Email Account
+
+
+
+
+
+ setNewAccount({ ...newAccount, username: e.target.value })}
+ placeholder="user"
+ />
+ The local part of the email address (before @)
+
+
+
+
+ setNewAccount({ ...newAccount, password: e.target.value })}
+ placeholder="Enter password"
+ style={{ flex: 1 }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Change Password Modal */}
+ {showChangePasswordModal && (
+
setShowChangePasswordModal(false)}>
+
e.stopPropagation()}>
+
+
Change Password
+
+
+
+
+
+
+ setNewPassword(e.target.value)}
+ placeholder="Enter new password"
+ style={{ flex: 1 }}
+ />
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Edit Quota Modal */}
+ {showEditQuotaModal && (
+
setShowEditQuotaModal(false)}>
+
e.stopPropagation()}>
+
+
Edit Quota
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Add Alias Modal */}
+ {showAddAliasModal && (
+
setShowAddAliasModal(false)}>
+
e.stopPropagation()}>
+
+
Add Alias
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Add Forwarding Modal */}
+ {showAddForwardingModal && (
+
setShowAddForwardingModal(false)}>
+
e.stopPropagation()}>
+
+
Add Forwarding Rule
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Add DNS Provider Modal */}
+ {showAddProviderModal && (
+
setShowAddProviderModal(false)}>
+
e.stopPropagation()}>
+
+
Add DNS Provider
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Confirm Dialog */}
+ {confirmDialog && (
+
+ )}
+
+ );
+}
+
+export default Email;
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index ccb6979..c584843 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -2905,6 +2905,168 @@ class ApiService {
const baseUrl = this.baseUrl.replace('/api/v1', '');
return `${baseUrl}/api/servers/agent/download/${os}/${arch}`;
}
+
+ // ── Email Server ──
+
+ async getEmailStatus() {
+ return this.request('/email/status');
+ }
+
+ async installEmailServer(data = {}) {
+ return this.request('/email/install', { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async controlEmailService(component, action) {
+ return this.request(`/email/service/${component}/${action}`, { method: 'POST' });
+ }
+
+ // Email Domains
+ async getEmailDomains() {
+ return this.request('/email/domains');
+ }
+
+ async addEmailDomain(data) {
+ return this.request('/email/domains', { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async getEmailDomain(domainId) {
+ return this.request(`/email/domains/${domainId}`);
+ }
+
+ async deleteEmailDomain(domainId) {
+ return this.request(`/email/domains/${domainId}`, { method: 'DELETE' });
+ }
+
+ async verifyEmailDNS(domainId) {
+ return this.request(`/email/domains/${domainId}/verify-dns`, { method: 'POST' });
+ }
+
+ async deployEmailDNS(domainId) {
+ return this.request(`/email/domains/${domainId}/deploy-dns`, { method: 'POST' });
+ }
+
+ // Email Accounts
+ async getEmailAccounts(domainId) {
+ return this.request(`/email/domains/${domainId}/accounts`);
+ }
+
+ async createEmailAccount(domainId, data) {
+ return this.request(`/email/domains/${domainId}/accounts`, { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async getEmailAccount(accountId) {
+ return this.request(`/email/accounts/${accountId}`);
+ }
+
+ async updateEmailAccount(accountId, data) {
+ return this.request(`/email/accounts/${accountId}`, { method: 'PUT', body: JSON.stringify(data) });
+ }
+
+ async deleteEmailAccount(accountId) {
+ return this.request(`/email/accounts/${accountId}`, { method: 'DELETE' });
+ }
+
+ async changeEmailPassword(accountId, password) {
+ return this.request(`/email/accounts/${accountId}/password`, { method: 'POST', body: JSON.stringify({ password }) });
+ }
+
+ // Email Aliases
+ async getEmailAliases(domainId) {
+ return this.request(`/email/domains/${domainId}/aliases`);
+ }
+
+ async createEmailAlias(domainId, data) {
+ return this.request(`/email/domains/${domainId}/aliases`, { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async deleteEmailAlias(aliasId) {
+ return this.request(`/email/aliases/${aliasId}`, { method: 'DELETE' });
+ }
+
+ // Email Forwarding
+ async getEmailForwarding(accountId) {
+ return this.request(`/email/accounts/${accountId}/forwarding`);
+ }
+
+ async createEmailForwarding(accountId, data) {
+ return this.request(`/email/accounts/${accountId}/forwarding`, { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async updateEmailForwarding(ruleId, data) {
+ return this.request(`/email/forwarding/${ruleId}`, { method: 'PUT', body: JSON.stringify(data) });
+ }
+
+ async deleteEmailForwarding(ruleId) {
+ return this.request(`/email/forwarding/${ruleId}`, { method: 'DELETE' });
+ }
+
+ // DNS Providers
+ async getEmailDNSProviders() {
+ return this.request('/email/dns-providers');
+ }
+
+ async addEmailDNSProvider(data) {
+ return this.request('/email/dns-providers', { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async deleteEmailDNSProvider(providerId) {
+ return this.request(`/email/dns-providers/${providerId}`, { method: 'DELETE' });
+ }
+
+ async testEmailDNSProvider(providerId) {
+ return this.request(`/email/dns-providers/${providerId}/test`, { method: 'POST' });
+ }
+
+ async getEmailDNSZones(providerId) {
+ return this.request(`/email/dns-providers/${providerId}/zones`);
+ }
+
+ // SpamAssassin
+ async getSpamConfig() {
+ return this.request('/email/spam/config');
+ }
+
+ async updateSpamConfig(data) {
+ return this.request('/email/spam/config', { method: 'PUT', body: JSON.stringify(data) });
+ }
+
+ async updateSpamRules() {
+ return this.request('/email/spam/update-rules', { method: 'POST' });
+ }
+
+ // Roundcube Webmail
+ async getWebmailStatus() {
+ return this.request('/email/webmail/status');
+ }
+
+ async installWebmail(data = {}) {
+ return this.request('/email/webmail/install', { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ async controlWebmail(action) {
+ return this.request(`/email/webmail/service/${action}`, { method: 'POST' });
+ }
+
+ async configureWebmailProxy(domain) {
+ return this.request('/email/webmail/configure-proxy', { method: 'POST', body: JSON.stringify({ domain }) });
+ }
+
+ // Mail Queue & Logs
+ async getMailQueue() {
+ return this.request('/email/queue');
+ }
+
+ async flushMailQueue() {
+ return this.request('/email/queue/flush', { method: 'POST' });
+ }
+
+ async deleteMailQueueItem(queueId) {
+ return this.request(`/email/queue/${queueId}`, { method: 'DELETE' });
+ }
+
+ async getMailLogs(lines = 100) {
+ return this.request(`/email/logs?lines=${lines}`);
+ }
}
export const api = new ApiService();
diff --git a/frontend/src/styles/main.less b/frontend/src/styles/main.less
index 9ba196c..319f0e8 100644
--- a/frontend/src/styles/main.less
+++ b/frontend/src/styles/main.less
@@ -67,6 +67,7 @@
@import 'pages/_settings';
@import 'pages/_file-manager';
@import 'pages/_ftp-server';
+@import 'pages/_email';
@import 'pages/_firewall';
@import 'pages/_cron';
@import 'pages/_security';
diff --git a/frontend/src/styles/pages/_email.less b/frontend/src/styles/pages/_email.less
new file mode 100644
index 0000000..c935870
--- /dev/null
+++ b/frontend/src/styles/pages/_email.less
@@ -0,0 +1,460 @@
+// Email Server Page Styles
+
+.email-server {
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ flex-wrap: wrap;
+ gap: 16px;
+
+ .page-header-content {
+ h1 {
+ font-size: 24px;
+ font-weight: 600;
+ margin: 0 0 4px 0;
+ }
+
+ .page-description {
+ color: var(--text-secondary);
+ margin: 0;
+ }
+ }
+
+ .page-header-actions {
+ display: flex;
+ gap: 8px;
+ }
+ }
+
+ .page-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 400px;
+ }
+
+ .empty-state-large {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 24px;
+ background: var(--card-bg);
+ border-radius: 12px;
+ text-align: center;
+
+ .icon {
+ font-size: 64px;
+ color: var(--text-tertiary);
+ margin-bottom: 24px;
+ }
+
+ h2 {
+ margin: 0 0 12px 0;
+ font-size: 24px;
+ font-weight: 600;
+ }
+
+ p {
+ margin: 0 0 24px 0;
+ color: var(--text-secondary);
+ max-width: 400px;
+ }
+ }
+
+ // Tab navigation
+ .tabs-nav {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 24px;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 0;
+ overflow-x: auto;
+
+ .tab-btn {
+ padding: 10px 16px;
+ border: none;
+ background: none;
+ color: var(--text-secondary);
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+ transition: all 0.2s;
+
+ &:hover {
+ color: var(--text-primary);
+ }
+
+ &.active {
+ color: var(--accent-primary);
+ border-bottom-color: var(--accent-primary);
+ }
+ }
+ }
+
+ // Status cards grid
+ .status-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin-bottom: 24px;
+ }
+
+ .status-card {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 20px;
+ background: var(--card-bg);
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+
+ .status-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+ flex-shrink: 0;
+
+ &.running { background: rgba(16, 185, 129, 0.1); color: #10b981; }
+ &.stopped { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
+ &.not-installed { background: rgba(156, 163, 175, 0.1); color: #9ca3af; }
+ }
+
+ .status-info {
+ flex: 1;
+ min-width: 0;
+
+ .status-name {
+ font-weight: 600;
+ font-size: 14px;
+ margin: 0 0 2px 0;
+ }
+
+ .status-detail {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin: 0;
+ }
+ }
+
+ .status-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+ }
+ }
+
+ // Stats row
+ .stats-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 16px;
+ margin-bottom: 24px;
+
+ .stat-card {
+ padding: 16px 20px;
+ background: var(--card-bg);
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ text-align: center;
+
+ .stat-value {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--text-primary);
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: 4px;
+ }
+ }
+ }
+
+ // Domain list
+ .domain-list, .account-list, .alias-list, .forwarding-list, .provider-list, .queue-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .domain-item, .account-item, .alias-item, .forwarding-item, .provider-item, .queue-item {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 20px;
+ background: var(--card-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ transition: border-color 0.2s;
+
+ &:hover {
+ border-color: var(--accent-primary);
+ }
+
+ .item-info {
+ flex: 1;
+ min-width: 0;
+
+ h4 {
+ margin: 0 0 4px 0;
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ .item-meta {
+ font-size: 12px;
+ color: var(--text-secondary);
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+ }
+
+ .item-badges {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+ }
+
+ .item-actions {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+ }
+ }
+
+ // DNS status badges
+ .dns-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+
+ &.valid {
+ background: rgba(16, 185, 129, 0.1);
+ color: #10b981;
+ }
+
+ &.invalid {
+ background: rgba(239, 68, 68, 0.1);
+ color: #ef4444;
+ }
+
+ &.unknown {
+ background: rgba(156, 163, 175, 0.1);
+ color: #9ca3af;
+ }
+ }
+
+ // Quota bar
+ .quota-bar {
+ width: 120px;
+ height: 6px;
+ background: var(--bg-hover);
+ border-radius: 3px;
+ overflow: hidden;
+ flex-shrink: 0;
+
+ .quota-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.3s;
+
+ &.low { background: #10b981; }
+ &.medium { background: #f59e0b; }
+ &.high { background: #ef4444; }
+ }
+ }
+
+ .quota-text {
+ font-size: 11px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ }
+
+ // DNS records display
+ .dns-records {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .dns-record-card {
+ padding: 16px;
+ background: var(--bg-hover);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+
+ .dns-record-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+
+ h4 {
+ margin: 0;
+ font-size: 13px;
+ font-weight: 600;
+ }
+ }
+
+ .dns-record-name {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 6px;
+ }
+
+ .dns-record-value {
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 12px;
+ background: var(--card-bg);
+ padding: 10px 12px;
+ border-radius: 6px;
+ word-break: break-all;
+ line-height: 1.5;
+ border: 1px solid var(--border-color);
+ }
+ }
+ }
+
+ // Spam config form
+ .spam-config-form {
+ max-width: 600px;
+
+ .config-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border-color);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .config-label {
+ font-size: 14px;
+ font-weight: 500;
+
+ .config-help {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-weight: 400;
+ display: block;
+ margin-top: 2px;
+ }
+ }
+ }
+ }
+
+ // Toggle switch
+ .toggle {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ border-radius: 12px;
+ background: var(--bg-hover);
+ border: 1px solid var(--border-color);
+ cursor: pointer;
+ transition: all 0.2s;
+ flex-shrink: 0;
+
+ &.active {
+ background: var(--accent-primary);
+ border-color: var(--accent-primary);
+
+ &::after {
+ transform: translateX(20px);
+ }
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: white;
+ transition: transform 0.2s;
+ }
+ }
+
+ // Logs display
+ .logs-container {
+ background: #1a1a2e;
+ color: #e0e0e0;
+ padding: 16px;
+ border-radius: 8px;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 12px;
+ line-height: 1.6;
+ max-height: 500px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ // Section headers inside cards
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ }
+ }
+
+ // Two column layout
+ .two-columns {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ // Copy button
+ .copy-btn {
+ padding: 4px 8px;
+ font-size: 11px;
+ background: var(--bg-hover);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ cursor: pointer;
+ color: var(--text-secondary);
+ transition: all 0.2s;
+
+ &:hover {
+ background: var(--accent-primary);
+ color: white;
+ border-color: var(--accent-primary);
+ }
+ }
+
+ // Arrow for alias display
+ .alias-arrow {
+ color: var(--text-tertiary);
+ font-size: 14px;
+ flex-shrink: 0;
+ }
+}
diff --git a/scripts/dev/start.sh b/scripts/dev/start.sh
index 3415f44..baaf474 100755
--- a/scripts/dev/start.sh
+++ b/scripts/dev/start.sh
@@ -10,8 +10,8 @@ echo " Frontend: http://localhost:5274"
echo ""
cd "$PROJECT_ROOT/backend"
-source venv/bin/activate
-python run.py &
+# Use venv's Python directly so we don't rely on PATH or 'source' (works with sh/bash)
+"$PROJECT_ROOT/backend/venv/bin/python" run.py &
BACKEND_PID=$!
sleep 2