2026-02-02 00:36:19 +00:00
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
|
import * as appstate from './appstate.js';
|
|
|
|
|
|
|
|
|
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
|
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
// Flat top-level views (no subviews)
|
2026-04-08 11:08:18 +00:00
|
|
|
const flatViews = ['logs'] as const;
|
2026-04-08 07:45:26 +00:00
|
|
|
|
|
|
|
|
// Tabbed views and their valid subviews
|
|
|
|
|
const subviewMap: Record<string, readonly string[]> = {
|
2026-04-08 08:24:55 +00:00
|
|
|
overview: ['stats', 'configuration'] as const,
|
|
|
|
|
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
|
|
|
|
|
email: ['log', 'security'] as const,
|
2026-04-08 09:01:08 +00:00
|
|
|
access: ['apitokens', 'users'] as const,
|
2026-04-08 08:24:55 +00:00
|
|
|
security: ['overview', 'blocked', 'authentication'] as const,
|
2026-04-08 11:08:18 +00:00
|
|
|
domains: ['providers', 'domains', 'dns', 'certificates'] as const,
|
2026-04-08 07:45:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Default subview when user visits the bare parent URL
|
|
|
|
|
const defaultSubview: Record<string, string> = {
|
2026-04-08 08:24:55 +00:00
|
|
|
overview: 'stats',
|
2026-04-08 07:45:26 +00:00
|
|
|
network: 'activity',
|
2026-04-08 08:24:55 +00:00
|
|
|
email: 'log',
|
|
|
|
|
access: 'apitokens',
|
2026-04-08 07:45:26 +00:00
|
|
|
security: 'overview',
|
2026-04-08 11:08:18 +00:00
|
|
|
domains: 'domains',
|
2026-04-08 07:45:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const validTopLevelViews = [...flatViews, ...Object.keys(subviewMap)] as const;
|
|
|
|
|
export type TValidView = typeof validTopLevelViews[number];
|
|
|
|
|
|
|
|
|
|
export function isValidView(view: string): boolean {
|
|
|
|
|
return (validTopLevelViews as readonly string[]).includes(view);
|
|
|
|
|
}
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
export function isValidSubview(view: string, subview: string): boolean {
|
|
|
|
|
return subviewMap[view]?.includes(subview) ?? false;
|
|
|
|
|
}
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
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 {
|
2026-04-08 07:45:26 +00:00
|
|
|
// Flat views
|
|
|
|
|
for (const view of flatViews) {
|
|
|
|
|
this.router.on(`/${view}`, async () => {
|
|
|
|
|
this.updateViewState(view, null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tabbed views
|
|
|
|
|
for (const view of Object.keys(subviewMap)) {
|
|
|
|
|
// Bare parent → redirect to default subview
|
2026-02-22 00:45:01 +00:00
|
|
|
this.router.on(`/${view}`, async () => {
|
2026-04-08 07:45:26 +00:00
|
|
|
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
2026-02-22 00:45:01 +00:00
|
|
|
});
|
2026-04-08 07:45:26 +00:00
|
|
|
// Each valid subview
|
|
|
|
|
for (const sub of subviewMap[view]) {
|
|
|
|
|
this.router.on(`/${view}/${sub}`, async () => {
|
|
|
|
|
this.updateViewState(view, sub);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Root redirect
|
|
|
|
|
this.router.on('/', async () => {
|
|
|
|
|
this.navigateTo('/overview');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupStateSync(): void {
|
2026-03-27 18:46:11 +00:00
|
|
|
appstate.uiStatePart.select().subscribe((uiState) => {
|
2026-02-02 00:36:19 +00:00
|
|
|
if (this.suppressStateUpdate) return;
|
|
|
|
|
|
|
|
|
|
const currentPath = window.location.pathname;
|
2026-04-08 07:45:26 +00:00
|
|
|
const expectedPath = uiState.activeSubview
|
|
|
|
|
? `/${uiState.activeView}/${uiState.activeSubview}`
|
|
|
|
|
: `/${uiState.activeView}`;
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2026-02-22 00:45:01 +00:00
|
|
|
if (currentPath !== expectedPath) {
|
2026-02-02 00:36:19 +00:00
|
|
|
this.suppressStateUpdate = true;
|
2026-02-22 00:45:01 +00:00
|
|
|
this.router.pushUrl(expectedPath);
|
2026-02-02 00:36:19 +00:00
|
|
|
this.suppressStateUpdate = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleInitialRoute(): void {
|
|
|
|
|
const path = window.location.pathname;
|
|
|
|
|
|
|
|
|
|
if (!path || path === '/') {
|
|
|
|
|
this.router.pushUrl('/overview');
|
2026-04-08 07:45:26 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segments = path.split('/').filter(Boolean);
|
|
|
|
|
const view = segments[0];
|
|
|
|
|
const sub = segments[1];
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
if (!isValidView(view)) {
|
|
|
|
|
this.router.pushUrl('/overview');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (subviewMap[view]) {
|
|
|
|
|
if (sub && isValidSubview(view, sub)) {
|
|
|
|
|
this.updateViewState(view, sub);
|
2026-02-02 00:36:19 +00:00
|
|
|
} else {
|
2026-04-08 07:45:26 +00:00
|
|
|
// Bare parent or invalid sub → default subview
|
|
|
|
|
this.router.pushUrl(`/${view}/${defaultSubview[view]}`);
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
2026-04-08 07:45:26 +00:00
|
|
|
} else {
|
|
|
|
|
this.updateViewState(view, null);
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
private updateViewState(view: string, subview: string | null): void {
|
2026-02-02 00:36:19 +00:00
|
|
|
this.suppressStateUpdate = true;
|
2026-03-26 07:40:56 +00:00
|
|
|
const currentState = appstate.uiStatePart.getState()!;
|
2026-04-08 07:45:26 +00:00
|
|
|
if (currentState.activeView !== view || currentState.activeSubview !== subview) {
|
2026-02-02 00:36:19 +00:00
|
|
|
appstate.uiStatePart.setState({
|
|
|
|
|
...currentState,
|
|
|
|
|
activeView: view,
|
2026-04-08 07:45:26 +00:00
|
|
|
activeSubview: subview,
|
2026-03-26 07:40:56 +00:00
|
|
|
} as appstate.IUiState);
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
|
|
|
|
this.suppressStateUpdate = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public navigateTo(path: string): void {
|
|
|
|
|
this.router.pushUrl(path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
public navigateToView(view: string, subview?: string): void {
|
|
|
|
|
if (!isValidView(view)) {
|
2026-02-02 00:36:19 +00:00
|
|
|
this.navigateTo('/overview');
|
2026-04-08 07:45:26 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (subview && isValidSubview(view, subview)) {
|
|
|
|
|
this.navigateTo(`/${view}/${subview}`);
|
|
|
|
|
} else if (subviewMap[view]) {
|
|
|
|
|
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
|
|
|
|
} else {
|
|
|
|
|
this.navigateTo(`/${view}`);
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getCurrentView(): string {
|
2026-03-26 07:40:56 +00:00
|
|
|
return appstate.uiStatePart.getState()!.activeView;
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public destroy(): void {
|
|
|
|
|
this.router.destroy();
|
|
|
|
|
this.initialized = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const appRouter = new AppRouter();
|