From 48d3d1218f9dab9d11ed47e38d19b62d2fcf5a9a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 22 Feb 2026 00:45:01 +0000 Subject: [PATCH] BREAKING CHANGE(email-ops): migrate email operations to catalog-compatible email model and simplify UI/router --- changelog.md | 10 + package.json | 1 + pnpm-lock.yaml | 19 + ts/00_commitinfo_data.ts | 2 +- ts/opsserver/handlers/email-ops.handler.ts | 414 +++++------ ts_interfaces/requests/email-ops.ts | 253 ++----- ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 229 +----- ts_web/elements/ops-view-emails.ts | 789 ++------------------- ts_web/elements/ops-view-logs.ts | 89 +-- ts_web/plugins.ts | 4 + ts_web/router.ts | 87 +-- 12 files changed, 338 insertions(+), 1561 deletions(-) diff --git a/changelog.md b/changelog.md index f750439..347ed0a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-22 - 8.0.0 - BREAKING CHANGE(email-ops) +migrate email operations to catalog-compatible email model and simplify UI/router + +- Add @serve.zone/catalog dependency and import (szCatalog) in web plugins +- Replace queue-based typedrequest methods with catalog APIs: getQueuedEmails / getSentEmails / getFailedEmails => getAllEmails and getEmailDetail (request/response shapes changed) +- Update TypeScript interfaces: IEmailQueueItem/IBounceRecord/ISecurityIncident etc. replaced by IEmail, IEmailDetail, ISmtpLogEntry, IConnectionInfo, IAuthenticationResults (breaking type changes) +- Frontend state and actions consolidated: emailOps state now holds emails array; multiple fetch actions removed and replaced by fetchAllEmailsAction and getEmailDetail usage +- UI components updated: ops-view-emails switched to list/detail view and now requests email detail via new API; router no longer exposes email folder routes and email-folder navigation removed +- Ops server handler refactored to return catalog-style emails and email detail; added status mapping and size formatting helpers + ## 2026-02-21 - 7.4.3 - fix(logging) add adaptive rate-limited DNS query logging, flush pending DNS logs on shutdown, and enhance email delivery logging diff --git a/package.json b/package.json index bd49b9f..d374a2a 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartstate": "^2.0.30", "@push.rocks/smartunique": "^3.0.9", + "@serve.zone/catalog": "^2.2.0", "@serve.zone/interfaces": "^5.3.0", "@serve.zone/remoteingress": "^4.0.0", "@tsclass/tsclass": "^9.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0397089..12799e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@push.rocks/smartunique': specifier: ^3.0.9 version: 3.0.9 + '@serve.zone/catalog': + specifier: ^2.2.0 + version: 2.2.0(@tiptap/pm@2.27.2) '@serve.zone/interfaces': specifier: ^5.3.0 version: 5.3.0 @@ -1330,6 +1333,9 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@serve.zone/catalog@2.2.0': + resolution: {integrity: sha512-FxRGjuz8PdOXnfjHAGuPWP4jUTVGl5r9rsnxZlGSgTT+dHAm6Ue9AoTCkwVTKV9hP/Ac4yy8KKeNtNYIlidfJQ==} + '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} @@ -6779,6 +6785,19 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@serve.zone/catalog@2.2.0(@tiptap/pm@2.27.2)': + dependencies: + '@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2) + '@design.estate/dees-domtools': 2.3.8 + '@design.estate/dees-element': 2.1.6 + '@design.estate/dees-wcctools': 3.8.0 + transitivePeerDependencies: + - '@nuxt/kit' + - '@tiptap/pm' + - react + - supports-color + - vue + '@serve.zone/interfaces@5.3.0': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 65402a8..32d755a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '7.4.3', + version: '8.0.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/opsserver/handlers/email-ops.handler.ts b/ts/opsserver/handlers/email-ops.handler.ts index 45f653d..48ada85 100644 --- a/ts/opsserver/handlers/email-ops.handler.ts +++ b/ts/opsserver/handlers/email-ops.handler.ts @@ -1,7 +1,6 @@ import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; -import { SecurityLogger } from '../../security/index.js'; export class EmailOpsHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -13,68 +12,24 @@ export class EmailOpsHandler { } private registerHandlers(): void { - // Get Queued Emails Handler + // Get All Emails Handler this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getQueuedEmails', + new plugins.typedrequest.TypedHandler( + 'getAllEmails', async (dataArg) => { - const emailServer = this.opsServerRef.dcRouterRef.emailServer; - if (!emailServer?.deliveryQueue) { - return { items: [], total: 0 }; - } - - const queue = emailServer.deliveryQueue; - const stats = queue.getStats(); - - // Get all queue items and filter by status if provided - const items = this.getQueueItems( - dataArg.status, - dataArg.limit || 50, - dataArg.offset || 0 - ); - - return { - items, - total: stats.queueSize, - }; + const emails = this.getAllQueueEmails(); + return { emails }; } ) ); - // Get Sent Emails Handler + // Get Email Detail Handler this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getSentEmails', + new plugins.typedrequest.TypedHandler( + 'getEmailDetail', async (dataArg) => { - const items = this.getQueueItems( - 'delivered', - dataArg.limit || 50, - dataArg.offset || 0 - ); - - return { - items, - total: items.length, // Note: total would ideally come from a counter - }; - } - ) - ); - - // Get Failed Emails Handler - this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getFailedEmails', - async (dataArg) => { - const items = this.getQueueItems( - 'failed', - dataArg.limit || 50, - dataArg.offset || 0 - ); - - return { - items, - total: items.length, - }; + const email = this.getEmailDetail(dataArg.emailId); + return { email }; } ) ); @@ -101,17 +56,12 @@ export class EmailOpsHandler { } try { - // Re-enqueue the failed email by creating a new queue entry - // with the same data but reset attempt count const newQueueId = await queue.enqueue( item.processingResult, item.processingMode, item.route ); - - // Optionally remove the old failed entry await queue.removeItem(dataArg.emailId); - return { success: true, newQueueId }; } catch (error) { return { @@ -122,197 +72,199 @@ export class EmailOpsHandler { } ) ); - - // Get Security Incidents Handler - this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getSecurityIncidents', - async (dataArg) => { - const securityLogger = SecurityLogger.getInstance(); - - const filter: { - level?: any; - type?: any; - } = {}; - - if (dataArg.level) { - filter.level = dataArg.level; - } - - if (dataArg.type) { - filter.type = dataArg.type; - } - - const incidents = securityLogger.getRecentEvents( - dataArg.limit || 100, - Object.keys(filter).length > 0 ? filter : undefined - ); - - return { - incidents: incidents.map(event => ({ - timestamp: event.timestamp, - level: event.level as interfaces.requests.TSecurityLogLevel, - type: event.type as interfaces.requests.TSecurityEventType, - message: event.message, - details: event.details, - ipAddress: event.ipAddress, - userId: event.userId, - sessionId: event.sessionId, - emailId: event.emailId, - domain: event.domain, - action: event.action, - result: event.result, - success: event.success, - })), - total: incidents.length, - }; - } - ) - ); - - // Get Bounce Records Handler - this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getBounceRecords', - async (dataArg) => { - const emailServer = this.opsServerRef.dcRouterRef.emailServer; - - if (!emailServer) { - return { records: [], suppressionList: [], total: 0 }; - } - - // Use smartmta's public API for bounce/suppression data - const suppressionList = emailServer.getSuppressionList(); - const hardBouncedAddresses = emailServer.getHardBouncedAddresses(); - - // Create bounce records from the available data - const records: interfaces.requests.IBounceRecord[] = []; - - for (const email of hardBouncedAddresses) { - const bounceInfo = emailServer.getBounceHistory(email); - if (bounceInfo) { - records.push({ - id: `bounce-${email}`, - recipient: email, - sender: '', - domain: email.split('@')[1] || '', - bounceType: (bounceInfo as any).type as interfaces.requests.TBounceType, - bounceCategory: (bounceInfo as any).category as interfaces.requests.TBounceCategory, - timestamp: (bounceInfo as any).lastBounce, - processed: true, - }); - } - } - - // Apply limit and offset - const limit = dataArg.limit || 50; - const offset = dataArg.offset || 0; - const paginatedRecords = records.slice(offset, offset + limit); - - return { - records: paginatedRecords, - suppressionList, - total: records.length, - }; - } - ) - ); - - // Remove from Suppression List Handler - this.typedrouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'removeFromSuppressionList', - async (dataArg) => { - const emailServer = this.opsServerRef.dcRouterRef.emailServer; - - if (!emailServer) { - return { success: false, error: 'Email server not available' }; - } - - try { - emailServer.removeFromSuppressionList(dataArg.email); - return { success: true }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to remove from suppression list' - }; - } - } - ) - ); } /** - * Helper method to get queue items with filtering and pagination + * Get all queue items mapped to catalog IEmail format */ - private getQueueItems( - status?: interfaces.requests.TEmailQueueStatus, - limit: number = 50, - offset: number = 0 - ): interfaces.requests.IEmailQueueItem[] { + private getAllQueueEmails(): interfaces.requests.IEmail[] { const emailServer = this.opsServerRef.dcRouterRef.emailServer; if (!emailServer?.deliveryQueue) { return []; } const queue = emailServer.deliveryQueue; - const items: interfaces.requests.IEmailQueueItem[] = []; - - // Access the internal queue map via reflection - // This is necessary because the queue doesn't expose iteration methods const queueMap = (queue as any).queue as Map; if (!queueMap) { return []; } - // Filter and convert items + const emails: interfaces.requests.IEmail[] = []; + for (const [id, item] of queueMap.entries()) { - // Apply status filter if provided - if (status && item.status !== status) { - continue; - } - - // Extract email details from processingResult if available - const processingResult = item.processingResult; - let from = ''; - let to: string[] = []; - let subject = ''; - - if (processingResult) { - // Check if it's an Email object or raw email data - if (processingResult.email) { - from = processingResult.email.from || ''; - to = processingResult.email.to || []; - subject = processingResult.email.subject || ''; - } else if (processingResult.from) { - from = processingResult.from; - to = processingResult.to || []; - subject = processingResult.subject || ''; - } - } - - items.push({ - id: item.id, - processingMode: item.processingMode, - status: item.status, - attempts: item.attempts, - nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt, - lastError: item.lastError, - createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt, - updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt, - deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt, - from, - to, - subject, - }); + emails.push(this.mapQueueItemToEmail(item)); } // Sort by createdAt descending (newest first) - items.sort((a, b) => b.createdAt - a.createdAt); + emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); - // Apply pagination - return items.slice(offset, offset + limit); + return emails; + } + + /** + * Get a single email detail by ID + */ + private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null { + const emailServer = this.opsServerRef.dcRouterRef.emailServer; + if (!emailServer?.deliveryQueue) { + return null; + } + + const queue = emailServer.deliveryQueue; + const item = queue.getItem(emailId); + + if (!item) { + return null; + } + + return this.mapQueueItemToEmailDetail(item); + } + + /** + * Map a queue item to catalog IEmail format + */ + private mapQueueItemToEmail(item: any): interfaces.requests.IEmail { + const processingResult = item.processingResult; + let from = ''; + let to = ''; + let subject = ''; + let messageId = ''; + let size = '0 B'; + + if (processingResult) { + if (processingResult.email) { + from = processingResult.email.from || ''; + to = (processingResult.email.to || [])[0] || ''; + subject = processingResult.email.subject || ''; + } else if (processingResult.from) { + from = processingResult.from; + to = (processingResult.to || [])[0] || ''; + subject = processingResult.subject || ''; + } + + // Try to get messageId + if (typeof processingResult.getMessageId === 'function') { + try { + messageId = processingResult.getMessageId() || ''; + } catch { + messageId = ''; + } + } + + // Compute approximate size + const textLen = processingResult.text?.length || 0; + const htmlLen = processingResult.html?.length || 0; + let attachSize = 0; + if (typeof processingResult.getAttachmentsSize === 'function') { + try { + attachSize = processingResult.getAttachmentsSize() || 0; + } catch { + attachSize = 0; + } + } + size = this.formatSize(textLen + htmlLen + attachSize); + } + + // Map queue status to catalog TEmailStatus + const status = this.mapStatus(item.status); + + const createdAt = item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt; + + return { + id: item.id, + direction: 'outbound' as interfaces.requests.TEmailDirection, + status, + from, + to, + subject, + timestamp: new Date(createdAt).toISOString(), + messageId, + size, + }; + } + + /** + * Map a queue item to catalog IEmailDetail format + */ + private mapQueueItemToEmailDetail(item: any): interfaces.requests.IEmailDetail { + const base = this.mapQueueItemToEmail(item); + const processingResult = item.processingResult; + + let toList: string[] = []; + let cc: string[] = []; + let headers: Record = {}; + let body = ''; + + if (processingResult) { + if (processingResult.email) { + toList = processingResult.email.to || []; + cc = processingResult.email.cc || []; + } else { + toList = processingResult.to || []; + cc = processingResult.cc || []; + } + + headers = processingResult.headers || {}; + body = processingResult.html || processingResult.text || ''; + } + + return { + ...base, + toList, + cc, + smtpLog: [], + connectionInfo: { + sourceIp: '', + sourceHostname: '', + destinationIp: '', + destinationPort: 0, + tlsVersion: '', + tlsCipher: '', + authenticated: false, + authMethod: '', + authUser: '', + }, + authenticationResults: { + spf: 'none', + spfDomain: '', + dkim: 'none', + dkimDomain: '', + dmarc: 'none', + dmarcPolicy: '', + }, + rejectionReason: item.status === 'failed' ? item.lastError : undefined, + bounceMessage: item.status === 'failed' ? item.lastError : undefined, + headers, + body, + }; + } + + /** + * Map queue status to catalog TEmailStatus + */ + private mapStatus(queueStatus: string): interfaces.requests.TEmailStatus { + switch (queueStatus) { + case 'pending': + case 'processing': + return 'pending'; + case 'delivered': + return 'delivered'; + case 'failed': + return 'bounced'; + case 'deferred': + return 'deferred'; + default: + return 'pending'; + } + } + + /** + * Format byte size to human-readable string + */ + private formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } } diff --git a/ts_interfaces/requests/email-ops.ts b/ts_interfaces/requests/email-ops.ts index 6591e9d..64d8a6a 100644 --- a/ts_interfaces/requests/email-ops.ts +++ b/ts_interfaces/requests/email-ops.ts @@ -2,162 +2,93 @@ import * as plugins from '../plugins.js'; import * as authInterfaces from '../data/auth.js'; // ============================================================================ -// Email Queue Item Interface (matches backend IQueueItem) +// Catalog-compatible email types (matches @serve.zone/catalog IEmail/IEmailDetail) // ============================================================================ -export type TEmailQueueStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred'; +export type TEmailStatus = 'delivered' | 'bounced' | 'rejected' | 'deferred' | 'pending'; +export type TEmailDirection = 'inbound' | 'outbound'; -export interface IEmailQueueItem { +export interface IEmail { id: string; - processingMode: 'forward' | 'mta' | 'process'; - status: TEmailQueueStatus; - attempts: number; - nextAttempt: number; // timestamp - lastError?: string; - createdAt: number; // timestamp - updatedAt: number; // timestamp - deliveredAt?: number; // timestamp - // Email details extracted from processingResult - from?: string; - to?: string[]; - subject?: string; + direction: TEmailDirection; + status: TEmailStatus; + from: string; + to: string; + subject: string; + timestamp: string; + messageId: string; + size: string; +} + +export interface ISmtpLogEntry { + timestamp: string; + direction: 'client' | 'server'; + command: string; + responseCode?: number; +} + +export interface IConnectionInfo { + sourceIp: string; + sourceHostname: string; + destinationIp: string; + destinationPort: number; + tlsVersion: string; + tlsCipher: string; + authenticated: boolean; + authMethod: string; + authUser: string; +} + +export interface IAuthenticationResults { + spf: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none'; + spfDomain: string; + dkim: 'pass' | 'fail' | 'none'; + dkimDomain: string; + dmarc: 'pass' | 'fail' | 'none'; + dmarcPolicy: string; +} + +export interface IEmailDetail extends IEmail { + toList: string[]; + cc?: string[]; + smtpLog: ISmtpLogEntry[]; + connectionInfo: IConnectionInfo; + authenticationResults: IAuthenticationResults; + rejectionReason?: string; + bounceMessage?: string; + headers: Record; + body: string; } // ============================================================================ -// Bounce Record Interface (matches backend BounceRecord) +// Get All Emails Request // ============================================================================ -export type TBounceType = - | 'invalid_recipient' - | 'domain_not_found' - | 'mailbox_full' - | 'mailbox_inactive' - | 'blocked' - | 'spam_related' - | 'policy_related' - | 'server_unavailable' - | 'temporary_failure' - | 'quota_exceeded' - | 'network_error' - | 'timeout' - | 'auto_response' - | 'challenge_response' - | 'unknown'; - -export type TBounceCategory = 'hard' | 'soft' | 'auto_response' | 'unknown'; - -export interface IBounceRecord { - id: string; - originalEmailId?: string; - recipient: string; - sender: string; - domain: string; - subject?: string; - bounceType: TBounceType; - bounceCategory: TBounceCategory; - timestamp: number; - smtpResponse?: string; - diagnosticCode?: string; - statusCode?: string; - processed: boolean; - retryCount?: number; - nextRetryTime?: number; -} - -// ============================================================================ -// Security Incident Interface (matches backend ISecurityEvent) -// ============================================================================ -export type TSecurityLogLevel = 'info' | 'warn' | 'error' | 'critical'; - -export type TSecurityEventType = - | 'authentication' - | 'access_control' - | 'email_validation' - | 'email_processing' - | 'email_forwarding' - | 'email_delivery' - | 'dkim' - | 'spf' - | 'dmarc' - | 'rate_limit' - | 'rate_limiting' - | 'spam' - | 'malware' - | 'connection' - | 'data_exposure' - | 'configuration' - | 'ip_reputation' - | 'rejected_connection'; - -export interface ISecurityIncident { - timestamp: number; - level: TSecurityLogLevel; - type: TSecurityEventType; - message: string; - details?: any; - ipAddress?: string; - userId?: string; - sessionId?: string; - emailId?: string; - domain?: string; - action?: string; - result?: string; - success?: boolean; -} - -// ============================================================================ -// Get Queued Emails Request -// ============================================================================ -export interface IReq_GetQueuedEmails extends plugins.typedrequestInterfaces.implementsTR< +export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implementsTR< plugins.typedrequestInterfaces.ITypedRequest, - IReq_GetQueuedEmails + IReq_GetAllEmails > { - method: 'getQueuedEmails'; + method: 'getAllEmails'; request: { identity?: authInterfaces.IIdentity; - status?: TEmailQueueStatus; - limit?: number; - offset?: number; }; response: { - items: IEmailQueueItem[]; - total: number; + emails: IEmail[]; }; } // ============================================================================ -// Get Sent Emails Request +// Get Email Detail Request // ============================================================================ -export interface IReq_GetSentEmails extends plugins.typedrequestInterfaces.implementsTR< +export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.implementsTR< plugins.typedrequestInterfaces.ITypedRequest, - IReq_GetSentEmails + IReq_GetEmailDetail > { - method: 'getSentEmails'; + method: 'getEmailDetail'; request: { identity?: authInterfaces.IIdentity; - limit?: number; - offset?: number; + emailId: string; }; response: { - items: IEmailQueueItem[]; - total: number; - }; -} - -// ============================================================================ -// Get Failed Emails Request -// ============================================================================ -export interface IReq_GetFailedEmails extends plugins.typedrequestInterfaces.implementsTR< - plugins.typedrequestInterfaces.ITypedRequest, - IReq_GetFailedEmails -> { - method: 'getFailedEmails'; - request: { - identity?: authInterfaces.IIdentity; - limit?: number; - offset?: number; - }; - response: { - items: IEmailQueueItem[]; - total: number; + email: IEmailDetail | null; }; } @@ -179,61 +110,3 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme error?: string; }; } - -// ============================================================================ -// Get Security Incidents Request -// ============================================================================ -export interface IReq_GetSecurityIncidents extends plugins.typedrequestInterfaces.implementsTR< - plugins.typedrequestInterfaces.ITypedRequest, - IReq_GetSecurityIncidents -> { - method: 'getSecurityIncidents'; - request: { - identity?: authInterfaces.IIdentity; - type?: TSecurityEventType; - level?: TSecurityLogLevel; - limit?: number; - }; - response: { - incidents: ISecurityIncident[]; - total: number; - }; -} - -// ============================================================================ -// Get Bounce Records Request -// ============================================================================ -export interface IReq_GetBounceRecords extends plugins.typedrequestInterfaces.implementsTR< - plugins.typedrequestInterfaces.ITypedRequest, - IReq_GetBounceRecords -> { - method: 'getBounceRecords'; - request: { - identity?: authInterfaces.IIdentity; - limit?: number; - offset?: number; - }; - response: { - records: IBounceRecord[]; - suppressionList: string[]; - total: number; - }; -} - -// ============================================================================ -// Remove from Suppression List Request -// ============================================================================ -export interface IReq_RemoveFromSuppressionList extends plugins.typedrequestInterfaces.implementsTR< - plugins.typedrequestInterfaces.ITypedRequest, - IReq_RemoveFromSuppressionList -> { - method: 'removeFromSuppressionList'; - request: { - identity?: authInterfaces.IIdentity; - email: string; - }; - response: { - success: boolean; - error?: string; - }; -} diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 65402a8..32d755a 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '7.4.3', + version: '8.0.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index b839eab..06e8c33 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -67,14 +67,7 @@ export interface ICertificateState { } export interface IEmailOpsState { - currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security'; - queuedEmails: interfaces.requests.IEmailQueueItem[]; - sentEmails: interfaces.requests.IEmailQueueItem[]; - failedEmails: interfaces.requests.IEmailQueueItem[]; - securityIncidents: interfaces.requests.ISecurityIncident[]; - bounceRecords: interfaces.requests.IBounceRecord[]; - suppressionList: string[]; - selectedEmailId: string | null; + emails: interfaces.requests.IEmail[]; isLoading: boolean; error: string | null; lastUpdated: number; @@ -165,14 +158,7 @@ export const networkStatePart = await appState.getStatePart( export const emailOpsStatePart = await appState.getStatePart( 'emailOps', { - currentView: 'queued', - queuedEmails: [], - sentEmails: [], - failedEmails: [], - securityIncidents: [], - bounceRecords: [], - suppressionList: [], - selectedEmailId: null, + emails: [], isLoading: false, error: null, lastUpdated: 0, @@ -492,35 +478,22 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat // Email Operations Actions // ============================================================================ -// Set Email Ops View Action -export const setEmailOpsViewAction = emailOpsStatePart.createAction( - async (statePartArg, view) => { - return { - ...statePartArg.getState(), - currentView: view, - }; - } -); - -// Fetch Queued Emails Action -export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => { +// Fetch All Emails Action +export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => { const context = getActionContext(); const currentState = statePartArg.getState(); try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetQueuedEmails - >('/typedrequest', 'getQueuedEmails'); + interfaces.requests.IReq_GetAllEmails + >('/typedrequest', 'getAllEmails'); const response = await request.fire({ identity: context.identity, - status: 'pending', - limit: 100, }); return { - ...currentState, - queuedEmails: response.items, + emails: response.emails, isLoading: false, error: null, lastUpdated: Date.now(), @@ -529,197 +502,11 @@ export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (sta return { ...currentState, isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch queued emails', + error: error instanceof Error ? error.message : 'Failed to fetch emails', }; } }); -// Fetch Sent Emails Action -export const fetchSentEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetSentEmails - >('/typedrequest', 'getSentEmails'); - - const response = await request.fire({ - identity: context.identity, - limit: 100, - }); - - return { - ...currentState, - sentEmails: response.items, - isLoading: false, - error: null, - lastUpdated: Date.now(), - }; - } catch (error) { - return { - ...currentState, - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch sent emails', - }; - } -}); - -// Fetch Failed Emails Action -export const fetchFailedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetFailedEmails - >('/typedrequest', 'getFailedEmails'); - - const response = await request.fire({ - identity: context.identity, - limit: 100, - }); - - return { - ...currentState, - failedEmails: response.items, - isLoading: false, - error: null, - lastUpdated: Date.now(), - }; - } catch (error) { - return { - ...currentState, - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch failed emails', - }; - } -}); - -// Fetch Security Incidents Action -export const fetchSecurityIncidentsAction = emailOpsStatePart.createAction(async (statePartArg) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetSecurityIncidents - >('/typedrequest', 'getSecurityIncidents'); - - const response = await request.fire({ - identity: context.identity, - limit: 100, - }); - - return { - ...currentState, - securityIncidents: response.incidents, - isLoading: false, - error: null, - lastUpdated: Date.now(), - }; - } catch (error) { - return { - ...currentState, - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch security incidents', - }; - } -}); - -// Fetch Bounce Records Action -export const fetchBounceRecordsAction = emailOpsStatePart.createAction(async (statePartArg) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetBounceRecords - >('/typedrequest', 'getBounceRecords'); - - const response = await request.fire({ - identity: context.identity, - limit: 100, - }); - - return { - ...currentState, - bounceRecords: response.records, - suppressionList: response.suppressionList, - isLoading: false, - error: null, - lastUpdated: Date.now(), - }; - } catch (error) { - return { - ...currentState, - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch bounce records', - }; - } -}); - -// Resend Failed Email Action -export const resendEmailAction = emailOpsStatePart.createAction(async (statePartArg, emailId) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_ResendEmail - >('/typedrequest', 'resendEmail'); - - const response = await request.fire({ - identity: context.identity, - emailId, - }); - - if (response.success) { - // Refresh failed emails list - await emailOpsStatePart.dispatchAction(fetchFailedEmailsAction, null); - await emailOpsStatePart.dispatchAction(fetchQueuedEmailsAction, null); - } - - return statePartArg.getState(); - } catch (error) { - return { - ...currentState, - error: error instanceof Error ? error.message : 'Failed to resend email', - }; - } -}); - -// Remove from Suppression List Action -export const removeFromSuppressionListAction = emailOpsStatePart.createAction( - async (statePartArg, email) => { - const context = getActionContext(); - const currentState = statePartArg.getState(); - - try { - const request = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_RemoveFromSuppressionList - >('/typedrequest', 'removeFromSuppressionList'); - - const response = await request.fire({ - identity: context.identity, - email, - }); - - if (response.success) { - // Refresh bounce records - await emailOpsStatePart.dispatchAction(fetchBounceRecordsAction, null); - } - - return statePartArg.getState(); - } catch (error) { - return { - ...currentState, - error: error instanceof Error ? error.message : 'Failed to remove from suppression list', - }; - } - } -); - // ============================================================================ // Certificate Actions // ============================================================================ diff --git a/ts_web/elements/ops-view-emails.ts b/ts_web/elements/ops-view-emails.ts index b0124d6..762badc 100644 --- a/ts_web/elements/ops-view-emails.ts +++ b/ts_web/elements/ops-view-emails.ts @@ -1,8 +1,8 @@ import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element'; +import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as shared from './shared/index.js'; import * as interfaces from '../../dist_ts_interfaces/index.js'; -import { appRouter } from '../router.js'; declare global { interface HTMLElementTagNameMap { @@ -10,67 +10,30 @@ declare global { } } -type TEmailFolder = 'queued' | 'sent' | 'failed' | 'received' | 'security'; - @customElement('ops-view-emails') export class OpsViewEmails extends DeesElement { @state() - accessor selectedFolder: TEmailFolder = 'queued'; + accessor emails: interfaces.requests.IEmail[] = []; @state() - accessor queuedEmails: interfaces.requests.IEmailQueueItem[] = []; + accessor selectedEmail: interfaces.requests.IEmailDetail | null = null; @state() - accessor sentEmails: interfaces.requests.IEmailQueueItem[] = []; - - @state() - accessor failedEmails: interfaces.requests.IEmailQueueItem[] = []; - - @state() - accessor securityIncidents: interfaces.requests.ISecurityIncident[] = []; - - @state() - accessor selectedEmail: interfaces.requests.IEmailQueueItem | null = null; - - @state() - accessor selectedIncident: interfaces.requests.ISecurityIncident | null = null; - - @state() - accessor showCompose = false; + accessor currentView: 'list' | 'detail' = 'list'; @state() accessor isLoading = false; - @state() - accessor searchTerm = ''; - - @state() - accessor emailDomains: string[] = []; - private stateSubscription: any; - constructor() { - super(); - this.loadData(); - this.loadEmailDomains(); - } - async connectedCallback() { await super.connectedCallback(); - // Subscribe to state changes this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => { - this.queuedEmails = state.queuedEmails; - this.sentEmails = state.sentEmails; - this.failedEmails = state.failedEmails; - this.securityIncidents = state.securityIncidents; + this.emails = state.emails; this.isLoading = state.isLoading; - - // Sync folder from state (e.g., when URL changes) - if (state.currentView !== this.selectedFolder) { - this.selectedFolder = state.currentView as TEmailFolder; - this.loadFolderData(state.currentView as TEmailFolder); - } }); + // Initial fetch + await appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, null); } async disconnectedCallback() { @@ -89,730 +52,58 @@ export class OpsViewEmails extends DeesElement { height: 100%; } - .emailLayout { - display: flex; - gap: 16px; + .viewContainer { height: 100%; - min-height: 600px; - } - - .sidebar { - flex-shrink: 0; - width: 280px; - } - - .mainArea { - flex: 1; - display: flex; - flex-direction: column; - gap: 16px; - overflow: hidden; - } - - .emailToolbar { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; - } - - .searchBox { - flex: 1; - min-width: 200px; - max-width: 400px; - } - - .emailList { - flex: 1; - overflow: hidden; - } - - .emailPreview { - flex: 1; - display: flex; - flex-direction: column; - background: ${cssManager.bdTheme('#fff', '#222')}; - border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; - border-radius: 8px; - overflow: hidden; - } - - .emailHeader { - padding: 24px; - border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; - } - - .emailSubject { - font-size: 24px; - font-weight: 600; - margin-bottom: 16px; - color: ${cssManager.bdTheme('#333', '#ccc')}; - } - - .emailMeta { - display: flex; - flex-direction: column; - gap: 8px; - font-size: 14px; - color: ${cssManager.bdTheme('#666', '#999')}; - } - - .emailMetaRow { - display: flex; - gap: 8px; - } - - .emailMetaLabel { - font-weight: 600; - min-width: 80px; - } - - .emailBody { - flex: 1; - padding: 24px; - overflow-y: auto; - font-size: 15px; - line-height: 1.6; - } - - .emailActions { - display: flex; - gap: 8px; - padding: 16px 24px; - border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; - background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')}; - } - - .emptyState { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 400px; - color: ${cssManager.bdTheme('#999', '#666')}; - } - - .emptyIcon { - font-size: 64px; - margin-bottom: 16px; - opacity: 0.3; - } - - .emptyText { - font-size: 18px; - } - - .status-pending { - color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')}; - } - - .status-processing { - color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; - } - - .status-delivered { - color: ${cssManager.bdTheme('#10b981', '#34d399')}; - } - - .status-failed { - color: ${cssManager.bdTheme('#ef4444', '#f87171')}; - } - - .status-deferred { - color: ${cssManager.bdTheme('#f97316', '#fb923c')}; - } - - .severity-info { - color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; - } - - .severity-warn { - color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')}; - } - - .severity-error { - color: ${cssManager.bdTheme('#ef4444', '#f87171')}; - } - - .severity-critical { - color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; - font-weight: bold; - } - - .incidentDetails { - padding: 24px; - background: ${cssManager.bdTheme('#fff', '#222')}; - border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; - border-radius: 8px; - } - - .incidentHeader { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; - } - - .incidentTitle { - font-size: 20px; - font-weight: 600; - } - - .incidentMeta { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; - margin-top: 16px; - } - - .incidentField { - padding: 12px; - background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; - border-radius: 6px; - } - - .incidentFieldLabel { - font-size: 12px; - color: ${cssManager.bdTheme('#666', '#999')}; - margin-bottom: 4px; - } - - .incidentFieldValue { - font-size: 14px; - word-break: break-all; } `, ]; public render() { - if (this.selectedEmail) { - return this.renderEmailDetail(); - } - - if (this.selectedIncident) { - return this.renderIncidentDetail(); - } - return html` Email Operations - - -
- this.openComposeModal()} type="highlighted"> - - Compose - - - this.searchTerm = (e.target as any).value} - > - - - - this.refreshData()}> - ${this.isLoading ? html`` : html``} - Refresh - - -
- - this.selectFolder('queued')} - .type=${this.selectedFolder === 'queued' ? 'highlighted' : 'normal'} - > - Queued ${this.queuedEmails.length > 0 ? `(${this.queuedEmails.length})` : ''} - - this.selectFolder('sent')} - .type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'} - > - Sent - - this.selectFolder('failed')} - .type=${this.selectedFolder === 'failed' ? 'highlighted' : 'normal'} - > - Failed ${this.failedEmails.length > 0 ? `(${this.failedEmails.length})` : ''} - - this.selectFolder('security')} - .type=${this.selectedFolder === 'security' ? 'highlighted' : 'normal'} - > - Security ${this.securityIncidents.length > 0 ? `(${this.securityIncidents.length})` : ''} - - -
+
+ ${this.currentView === 'detail' && this.selectedEmail + ? html` + + ` + : html` + + ` + }
- - ${this.renderContent()} `; } - private renderContent() { - switch (this.selectedFolder) { - case 'queued': - return this.renderEmailTable(this.queuedEmails, 'Queued Emails', 'Emails waiting to be delivered'); - case 'sent': - return this.renderEmailTable(this.sentEmails, 'Sent Emails', 'Successfully delivered emails'); - case 'failed': - return this.renderEmailTable(this.failedEmails, 'Failed Emails', 'Emails that failed to deliver', true); - case 'security': - return this.renderSecurityIncidents(); - default: - return this.renderEmptyState('Select a folder'); - } - } + private async handleEmailClick(e: CustomEvent) { + const emailSummary = e.detail; + try { + const context = appstate.loginStatePart.getState(); + const request = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_GetEmailDetail + >('/typedrequest', 'getEmailDetail'); - private renderEmailTable( - emails: interfaces.requests.IEmailQueueItem[], - heading1: string, - heading2: string, - showResend = false - ) { - const filteredEmails = this.filterEmails(emails); - - if (filteredEmails.length === 0) { - return this.renderEmptyState(`No emails in ${this.selectedFolder}`); - } - - const actions = [ - { - name: 'View Details', - iconName: 'lucide:eye', - type: ['doubleClick', 'inRow'] as any, - actionFunc: async (actionData: any) => { - this.selectedEmail = actionData.item; - } - } - ]; - - if (showResend) { - actions.push({ - name: 'Resend', - iconName: 'lucide:send', - type: ['inRow'] as any, - actionFunc: async (actionData: any) => { - await this.resendEmail(actionData.item.id); - } + const response = await request.fire({ + identity: context.identity, + emailId: emailSummary.id, }); - } - return html` - ({ - 'Status': html`${email.status}`, - 'From': email.from || 'N/A', - 'To': email.to?.join(', ') || 'N/A', - 'Subject': email.subject || 'No subject', - 'Attempts': email.attempts, - 'Created': this.formatDate(email.createdAt), - })} - .dataActions=${actions} - .selectionMode=${'single'} - heading1=${heading1} - heading2=${`${filteredEmails.length} emails - ${heading2}`} - > - `; - } - - private renderSecurityIncidents() { - const incidents = this.securityIncidents; - - if (incidents.length === 0) { - return this.renderEmptyState('No security incidents'); - } - - return html` - ({ - 'Severity': html`${incident.level.toUpperCase()}`, - 'Type': incident.type, - 'Message': incident.message, - 'IP': incident.ipAddress || 'N/A', - 'Domain': incident.domain || 'N/A', - 'Time': this.formatDate(incident.timestamp), - })} - .dataActions=${[ - { - name: 'View Details', - iconName: 'lucide:eye', - type: ['doubleClick', 'inRow'], - actionFunc: async (actionData: any) => { - this.selectedIncident = actionData.item; - } - } - ]} - .selectionMode=${'single'} - heading1="Security Incidents" - heading2=${`${incidents.length} incidents`} - > - `; - } - - private renderEmailDetail() { - if (!this.selectedEmail) return ''; - - return html` - Email Details -
- -
-
-
-
${this.selectedEmail.subject || 'No subject'}
-
-
- Status: - ${this.selectedEmail.status} -
-
- From: - ${this.selectedEmail.from || 'N/A'} -
-
- To: - ${this.selectedEmail.to?.join(', ') || 'N/A'} -
-
- Mode: - ${this.selectedEmail.processingMode} -
-
- Attempts: - ${this.selectedEmail.attempts} -
-
- Created: - ${new Date(this.selectedEmail.createdAt).toLocaleString()} -
- ${this.selectedEmail.deliveredAt ? html` -
- Delivered: - ${new Date(this.selectedEmail.deliveredAt).toLocaleString()} -
- ` : ''} - ${this.selectedEmail.lastError ? html` -
- Last Error: - ${this.selectedEmail.lastError} -
- ` : ''} -
-
- -
- ${this.selectedEmail.status === 'failed' ? html` - this.resendEmail(this.selectedEmail!.id)} type="highlighted"> - - Resend - - ` : ''} - this.selectedEmail = null}> - - Close - -
-
-
-
- `; - } - - private renderIncidentDetail() { - if (!this.selectedIncident) return ''; - - const incident = this.selectedIncident; - - return html` - Security Incident Details -
- this.selectedIncident = null} type="secondary"> - - Back to List - -
-
-
-
-
${incident.message}
-
- ${new Date(incident.timestamp).toLocaleString()} -
-
- - ${incident.level.toUpperCase()} - -
- -
-
-
Type
-
${incident.type}
-
- ${incident.ipAddress ? html` -
-
IP Address
-
${incident.ipAddress}
-
- ` : ''} - ${incident.domain ? html` -
-
Domain
-
${incident.domain}
-
- ` : ''} - ${incident.emailId ? html` -
-
Email ID
-
${incident.emailId}
-
- ` : ''} - ${incident.userId ? html` -
-
User ID
-
${incident.userId}
-
- ` : ''} - ${incident.action ? html` -
-
Action
-
${incident.action}
-
- ` : ''} - ${incident.result ? html` -
-
Result
-
${incident.result}
-
- ` : ''} - ${incident.success !== undefined ? html` -
-
Success
-
${incident.success ? 'Yes' : 'No'}
-
- ` : ''} -
- - ${incident.details ? html` -
-
Details
-
-${JSON.stringify(incident.details, null, 2)}
-            
-
- ` : ''} -
- `; - } - - private renderEmptyState(message: string) { - return html` -
- -
${message}
-
- `; - } - - private async openComposeModal() { - const { DeesModal } = await import('@design.estate/dees-catalog'); - - // Ensure domains are loaded before opening modal - if (this.emailDomains.length === 0) { - await this.loadEmailDomains(); - } - - await DeesModal.createAndShow({ - heading: 'New Email', - width: 'large', - content: html` -
- { - await this.sendEmail(e.detail); - const modals = document.querySelectorAll('dees-modal'); - modals.forEach(m => (m as any).destroy?.()); - }}> -
- - @ - 0 - ? this.emailDomains.map(domain => ({ key: domain, value: domain })) - : [{ key: 'dcrouter.local', value: 'dcrouter.local' }]} - .selectedKey=${this.emailDomains[0] || 'dcrouter.local'} - required - style="flex: 1;" - > -
- - - - - - - - -
-
- `, - menuOptions: [ - { - name: 'Send', - iconName: 'lucide:send', - action: async (modalArg) => { - const form = modalArg.shadowRoot?.querySelector('dees-form') as any; - form?.submit(); - } - }, - { - name: 'Cancel', - iconName: 'lucide:x', - action: async (modalArg) => await modalArg.destroy() - } - ] - }); - } - - private filterEmails(emails: interfaces.requests.IEmailQueueItem[]): interfaces.requests.IEmailQueueItem[] { - if (!this.searchTerm) { - return emails; - } - - const search = this.searchTerm.toLowerCase(); - return emails.filter(e => - (e.subject?.toLowerCase().includes(search)) || - (e.from?.toLowerCase().includes(search)) || - (e.to?.some(t => t.toLowerCase().includes(search))) - ); - } - - private selectFolder(folder: TEmailFolder) { - // Use router for navigation to update URL - appRouter.navigateToEmailFolder(folder); - // Clear selections - this.selectedEmail = null; - this.selectedIncident = null; - } - - private formatDate(timestamp: number): string { - const date = new Date(timestamp); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const hours = diff / (1000 * 60 * 60); - - if (hours < 24) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else if (hours < 168) { // 7 days - return date.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' }); - } else { - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); - } - } - - private async loadData() { - this.isLoading = true; - await this.loadFolderData(this.selectedFolder); - this.isLoading = false; - } - - private async loadFolderData(folder: TEmailFolder) { - switch (folder) { - case 'queued': - await appstate.emailOpsStatePart.dispatchAction(appstate.fetchQueuedEmailsAction, null); - break; - case 'sent': - await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSentEmailsAction, null); - break; - case 'failed': - await appstate.emailOpsStatePart.dispatchAction(appstate.fetchFailedEmailsAction, null); - break; - case 'security': - await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSecurityIncidentsAction, null); - break; - } - } - - private async loadEmailDomains() { - try { - await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); - const config = appstate.configStatePart.getState().config; - - if (config?.email?.domains && Array.isArray(config.email.domains) && config.email.domains.length > 0) { - this.emailDomains = config.email.domains; - } else { - this.emailDomains = ['dcrouter.local']; + if (response.email) { + this.selectedEmail = response.email; + this.currentView = 'detail'; } } catch (error) { - console.error('Failed to load email domains:', error); - this.emailDomains = ['dcrouter.local']; + console.error('Failed to fetch email detail:', error); } } - private async refreshData() { - this.isLoading = true; - await this.loadFolderData(this.selectedFolder); - this.isLoading = false; - } - - private async sendEmail(formData: any) { - try { - console.log('Sending email:', formData); - // TODO: Implement actual email sending via API - // For now, just log the data - const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`; - console.log('From:', fromEmail); - console.log('To:', formData.to); - console.log('Subject:', formData.subject); - } catch (error: any) { - console.error('Failed to send email', error); - } - } - - private async resendEmail(emailId: string) { - try { - await appstate.emailOpsStatePart.dispatchAction(appstate.resendEmailAction, emailId); - this.selectedEmail = null; - } catch (error) { - console.error('Failed to resend email:', error); - } + private handleBack() { + this.selectedEmail = null; + this.currentView = 'list'; } } diff --git a/ts_web/elements/ops-view-logs.ts b/ts_web/elements/ops-view-logs.ts index 11313b3..3c79812 100644 --- a/ts_web/elements/ops-view-logs.ts +++ b/ts_web/elements/ops-view-logs.ts @@ -1,4 +1,3 @@ -import * as plugins from '../plugins.js'; import * as shared from './shared/index.js'; import * as appstate from '../appstate.js'; @@ -20,15 +19,6 @@ export class OpsViewLogs extends DeesElement { filters: {}, }; - @state() - accessor filterLevel: string | undefined; - - @state() - accessor filterCategory: string | undefined; - - @state() - accessor filterLimit: number = 100; - private lastPushedCount = 0; constructor() { @@ -44,63 +34,13 @@ export class OpsViewLogs extends DeesElement { public static styles = [ cssManager.defaultStyles, shared.viewHostCss, - css` - .controls { - display: flex; - gap: 16px; - margin-bottom: 24px; - flex-wrap: wrap; - } - - .filterGroup { - display: flex; - align-items: center; - gap: 8px; - } - `, + css``, ]; public render() { return html` Logs -
-
- this.fetchLogs()} - > - Refresh Logs - -
- -
- - this.updateFilter('level', e.detail)} - > -
- -
- - this.updateFilter('category', e.detail)} - > -
- -
- - this.updateFilter('limit', e.detail)} - > -
-
- ; @@ -27,31 +25,10 @@ class AppRouter { } private setupRoutes(): void { - // Main views for (const view of validViews) { - if (view === 'emails') { - // Email root - default to queued - this.router.on('/emails', async () => { - this.updateViewState('emails'); - this.updateEmailFolder('queued'); - }); - - // Email with folder parameter - this.router.on('/emails/:folder', async (routeInfo) => { - const folder = routeInfo.params.folder as string; - if (validEmailFolders.includes(folder as TValidEmailFolder)) { - this.updateViewState('emails'); - this.updateEmailFolder(folder as TValidEmailFolder); - } else { - // Invalid folder, redirect to queued - this.navigateTo('/emails/queued'); - } - }); - } else { - this.router.on(`/${view}`, async () => { - this.updateViewState(view); - }); - } + this.router.on(`/${view}`, async () => { + this.updateViewState(view); + }); } // Root redirect @@ -61,60 +38,32 @@ class AppRouter { } private setupStateSync(): void { - // Sync URL when state changes programmatically (not from router) appstate.uiStatePart.state.subscribe((uiState) => { if (this.suppressStateUpdate) return; const currentPath = window.location.pathname; - const expectedPath = this.getExpectedPath(uiState.activeView); + const expectedPath = `/${uiState.activeView}`; - // Only update URL if it doesn't match current state - if (!currentPath.startsWith(expectedPath)) { + if (currentPath !== expectedPath) { this.suppressStateUpdate = true; - if (uiState.activeView === 'emails') { - const emailState = appstate.emailOpsStatePart.getState(); - this.router.pushUrl(`/emails/${emailState.currentView}`); - } else { - this.router.pushUrl(`/${uiState.activeView}`); - } + this.router.pushUrl(expectedPath); this.suppressStateUpdate = false; } }); } - private getExpectedPath(view: string): string { - if (view === 'emails') { - return '/emails'; - } - return `/${view}`; - } - private handleInitialRoute(): void { const path = window.location.pathname; if (!path || path === '/') { - // Redirect root to overview this.router.pushUrl('/overview'); } else { - // Parse current path and update state const segments = path.split('/').filter(Boolean); const view = segments[0]; if (validViews.includes(view as TValidView)) { this.updateViewState(view as TValidView); - - if (view === 'emails' && segments[1]) { - const folder = segments[1]; - if (validEmailFolders.includes(folder as TValidEmailFolder)) { - this.updateEmailFolder(folder as TValidEmailFolder); - } else { - this.updateEmailFolder('queued'); - } - } else if (view === 'emails') { - this.updateEmailFolder('queued'); - } } else { - // Invalid view, redirect to overview this.router.pushUrl('/overview'); } } @@ -132,18 +81,6 @@ class AppRouter { this.suppressStateUpdate = false; } - private updateEmailFolder(folder: TValidEmailFolder): void { - this.suppressStateUpdate = true; - const currentState = appstate.emailOpsStatePart.getState(); - if (currentState.currentView !== folder) { - appstate.emailOpsStatePart.setState({ - ...currentState, - currentView: folder as appstate.IEmailOpsState['currentView'], - }); - } - this.suppressStateUpdate = false; - } - public navigateTo(path: string): void { this.router.pushUrl(path); } @@ -156,22 +93,10 @@ class AppRouter { } } - public navigateToEmailFolder(folder: string): void { - if (validEmailFolders.includes(folder as TValidEmailFolder)) { - this.navigateTo(`/emails/${folder}`); - } else { - this.navigateTo('/emails/queued'); - } - } - public getCurrentView(): string { return appstate.uiStatePart.getState().activeView; } - public getCurrentEmailFolder(): string { - return appstate.emailOpsStatePart.getState().currentView; - } - public destroy(): void { this.router.destroy(); this.initialized = false;