BREAKING CHANGE(deps): upgrade major dependencies, migrate action.target to action.targets (array), adapt to SmartRequest API changes, and add RADIUS server support

This commit is contained in:
2026-02-02 00:36:19 +00:00
parent badabe753a
commit 1a108fa8b7
26 changed files with 1808 additions and 582 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '2.13.0',
version: '3.0.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -53,6 +53,20 @@ export interface INetworkState {
error: string | null;
}
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;
isLoading: boolean;
error: string | null;
lastUpdated: number;
}
// Create state parts with appropriate persistence
export const loginStatePart = await appState.getStatePart<ILoginState>(
'login',
@@ -60,7 +74,7 @@ export const loginStatePart = await appState.getStatePart<ILoginState>(
identity: null,
isLoggedIn: false,
},
'soft' // Login state persists across sessions
'persistent' // Login state persists across browser sessions
);
export const statsStatePart = await appState.getStatePart<IStatsState>(
@@ -121,6 +135,24 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
'soft'
);
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
'emailOps',
{
currentView: 'queued',
queuedEmails: [],
sentEmails: [],
failedEmails: [],
securityIncidents: [],
bounceRecords: [],
suppressionList: [],
selectedEmailId: null,
isLoading: false,
error: null,
lastUpdated: 0,
},
'soft'
);
// Actions for state management
interface IActionContext {
identity: interfaces.data.IIdentity | null;
@@ -397,6 +429,238 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
}
});
// ============================================================================
// Email Operations Actions
// ============================================================================
// Set Email Ops View Action
export const setEmailOpsViewAction = emailOpsStatePart.createAction<IEmailOpsState['currentView']>(
async (statePartArg, view) => {
return {
...statePartArg.getState(),
currentView: view,
};
}
);
// Fetch Queued Emails Action
export const fetchQueuedEmailsAction = 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');
const response = await request.fire({
identity: context.identity,
status: 'pending',
limit: 100,
});
return {
...currentState,
queuedEmails: response.items,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch queued 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<string>(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<string>(
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',
};
}
}
);
// Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() {
const context = getActionContext();

View File

@@ -1,5 +1,6 @@
import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import { appRouter } from '../router.js';
import {
DeesElement,
@@ -84,17 +85,55 @@ export class OpsDashboard extends DeesElement {
.select((stateArg) => stateArg)
.subscribe((uiState) => {
this.uiState = uiState;
// Sync appdash view when state changes (e.g., from URL navigation)
this.syncAppdashView(uiState.activeView);
});
this.rxSubscriptions.push(uiSubscription);
}
/**
* Sync the dees-simple-appdash view selection with the current state.
* This is needed when the URL changes and we need to update the UI.
*/
private syncAppdashView(viewName: string): void {
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
if (!appDash) return;
const targetTab = this.viewTabs.find(t => t.name.toLowerCase() === viewName);
if (!targetTab) return;
// Check if we need to switch (avoid unnecessary updates)
if (appDash.selectedView === targetTab) return;
// Update the selected view programmatically
appDash.selectedView = targetTab;
// Update the displayed content
const content = appDash.shadowRoot?.querySelector('.appcontent');
if (content) {
if (appDash.currentView) {
appDash.currentView.remove();
}
const view = new targetTab.element();
content.appendChild(view);
appDash.currentView = view;
}
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100vh;
overflow: hidden;
}
.maincontainer {
position: relative;
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
}
`,
];
@@ -126,24 +165,31 @@ export class OpsDashboard extends DeesElement {
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => {
const viewName = e.detail.view.name;
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
const viewName = e.detail.view.name.toLowerCase();
// Use router for navigation instead of direct state update
appRouter.navigateToView(viewName);
});
// Handle logout event
appDash.addEventListener('logout', async () => {
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
});
}
// Handle initial state
// Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState();
// Check initial login state
if (loginState.identity) {
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
if (loginState.identity?.jwt) {
// Verify JWT hasn't expired
if (loginState.identity.expiresAt > Date.now()) {
// JWT still valid, restore logged-in state
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
// JWT expired, clear the stored state
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
}
}

View File

@@ -155,11 +155,10 @@ export class OpsViewConfig extends DeesElement {
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
</div>
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)}
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)}
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)}
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)}
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
${this.renderConfigSection('email', 'Email Configuration', this.configState.config?.email)}
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config?.dns)}
${this.renderConfigSection('proxy', 'Proxy Configuration', this.configState.config?.proxy)}
${this.renderConfigSection('security', 'Security Configuration', this.configState.config?.security)}
` : html`
<div class="errorMessage">No configuration loaded</div>
`}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,10 @@ import * as plugins from './plugins.js';
import { html } from '@design.estate/dees-element';
import './elements/index.js';
import { appRouter } from './router.js';
// Initialize router before rendering
appRouter.init();
plugins.deesElement.render(html`
<ops-dashboard></ops-dashboard>

181
ts_web/router.ts Normal file
View File

@@ -0,0 +1,181 @@
import * as plugins from './plugins.js';
import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'] as const;
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
export type TValidView = typeof validViews[number];
export type TValidEmailFolder = typeof validEmailFolders[number];
class AppRouter {
private router: InstanceType<typeof SmartRouter>;
private initialized = false;
private suppressStateUpdate = false;
constructor() {
this.router = new SmartRouter({ debug: false });
}
public init(): void {
if (this.initialized) return;
this.setupRoutes();
this.setupStateSync();
this.handleInitialRoute();
this.initialized = true;
}
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);
});
}
}
// Root redirect
this.router.on('/', async () => {
this.navigateTo('/overview');
});
}
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);
// Only update URL if it doesn't match current state
if (!currentPath.startsWith(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.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');
}
}
}
private updateViewState(view: string): void {
this.suppressStateUpdate = true;
const currentState = appstate.uiStatePart.getState();
if (currentState.activeView !== view) {
appstate.uiStatePart.setState({
...currentState,
activeView: view,
});
}
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);
}
public navigateToView(view: string): void {
if (validViews.includes(view as TValidView)) {
this.navigateTo(`/${view}`);
} else {
this.navigateTo('/overview');
}
}
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;
}
}
export const appRouter = new AppRouter();