mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-12 22:46:58 +03:00
refactor: make dashboard widgets reactive
This commit is contained in:
@@ -3,9 +3,7 @@ import {
|
||||
getPodkopStatus,
|
||||
getSingboxStatus,
|
||||
} from '../../methods';
|
||||
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
|
||||
import { getClashWsUrl, onMount } from '../../../helpers';
|
||||
import { renderDashboardWidget } from './renderer/renderWidget';
|
||||
import {
|
||||
triggerLatencyGroupTest,
|
||||
triggerLatencyProxyTest,
|
||||
@@ -14,63 +12,114 @@ import {
|
||||
import { store, StoreType } from '../../../store';
|
||||
import { socket } from '../../../socket';
|
||||
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||
import { renderEmptyOutboundGroup } from './renderer/renderEmptyOutboundGroup';
|
||||
import { renderSections } from './renderSections';
|
||||
import { renderWidget } from './renderWidget';
|
||||
|
||||
// Fetchers
|
||||
|
||||
async function fetchDashboardSections() {
|
||||
const prev = store.get().sectionsWidget;
|
||||
|
||||
store.set({
|
||||
dashboardSections: {
|
||||
...store.get().dashboardSections,
|
||||
sectionsWidget: {
|
||||
...prev,
|
||||
failed: false,
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { data, success } = await getDashboardSections();
|
||||
|
||||
store.set({ dashboardSections: { loading: false, data, failed: !success } });
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
loading: false,
|
||||
failed: !success,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchServicesInfo() {
|
||||
const podkop = await getPodkopStatus();
|
||||
const singbox = await getSingboxStatus();
|
||||
const [podkop, singbox] = await Promise.all([
|
||||
getPodkopStatus(),
|
||||
getSingboxStatus(),
|
||||
]);
|
||||
|
||||
store.set({
|
||||
services: {
|
||||
singbox: singbox.running,
|
||||
podkop: podkop.enabled,
|
||||
servicesInfoWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: { singbox: singbox.running, podkop: podkop.enabled },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function connectToClashSockets() {
|
||||
socket.subscribe(`${getClashWsUrl()}/traffic?token=`, (msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
socket.subscribe(
|
||||
`${getClashWsUrl()}/traffic?token=`,
|
||||
(msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
|
||||
store.set({
|
||||
traffic: { up: parsedMsg.up, down: parsedMsg.down },
|
||||
});
|
||||
});
|
||||
store.set({
|
||||
bandwidthWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: { up: parsedMsg.up, down: parsedMsg.down },
|
||||
},
|
||||
});
|
||||
},
|
||||
(_err) => {
|
||||
store.set({
|
||||
bandwidthWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: { up: 0, down: 0 },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
socket.subscribe(`${getClashWsUrl()}/connections?token=`, (msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
socket.subscribe(
|
||||
`${getClashWsUrl()}/connections?token=`,
|
||||
(msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
|
||||
store.set({
|
||||
connections: {
|
||||
connections: parsedMsg.connections,
|
||||
downloadTotal: parsedMsg.downloadTotal,
|
||||
uploadTotal: parsedMsg.uploadTotal,
|
||||
memory: parsedMsg.memory,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
socket.subscribe(`${getClashWsUrl()}/memory?token=`, (msg) => {
|
||||
store.set({
|
||||
memory: { inuse: msg.inuse, oslimit: msg.oslimit },
|
||||
});
|
||||
});
|
||||
store.set({
|
||||
trafficTotalWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: {
|
||||
downloadTotal: parsedMsg.downloadTotal,
|
||||
uploadTotal: parsedMsg.uploadTotal,
|
||||
},
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: {
|
||||
connections: parsedMsg.connections?.length,
|
||||
memory: parsedMsg.memory,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
(_err) => {
|
||||
store.set({
|
||||
trafficTotalWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: {
|
||||
connections: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handlers
|
||||
@@ -104,18 +153,31 @@ function replaceTestLatencyButtonsWithSkeleton() {
|
||||
|
||||
// Renderer
|
||||
|
||||
async function renderDashboardSections() {
|
||||
const dashboardSections = store.get().dashboardSections;
|
||||
async function renderSectionsWidget() {
|
||||
console.log('renderSectionsWidget');
|
||||
const sectionsWidget = store.get().sectionsWidget;
|
||||
const container = document.getElementById('dashboard-sections-grid');
|
||||
|
||||
if (dashboardSections.failed) {
|
||||
const rendered = renderEmptyOutboundGroup();
|
||||
|
||||
return container!.replaceChildren(rendered);
|
||||
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||
const renderedWidget = renderSections({
|
||||
loading: sectionsWidget.loading,
|
||||
failed: sectionsWidget.failed,
|
||||
section: {
|
||||
code: '',
|
||||
displayName: '',
|
||||
outbounds: [],
|
||||
withTagSelect: false,
|
||||
},
|
||||
onTestLatency: () => {},
|
||||
onChooseOutbound: () => {},
|
||||
});
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedOutboundGroups = dashboardSections.data.map((section) =>
|
||||
renderOutboundGroup({
|
||||
const renderedWidgets = sectionsWidget.data.map((section) =>
|
||||
renderSections({
|
||||
loading: sectionsWidget.loading,
|
||||
failed: sectionsWidget.failed,
|
||||
section,
|
||||
onTestLatency: (tag) => {
|
||||
replaceTestLatencyButtonsWithSkeleton();
|
||||
@@ -132,18 +194,33 @@ async function renderDashboardSections() {
|
||||
}),
|
||||
);
|
||||
|
||||
container!.replaceChildren(...renderedOutboundGroups);
|
||||
return container!.replaceChildren(...renderedWidgets);
|
||||
}
|
||||
|
||||
async function renderTrafficWidget() {
|
||||
const traffic = store.get().traffic;
|
||||
async function renderBandwidthWidget() {
|
||||
console.log('renderBandwidthWidget');
|
||||
const traffic = store.get().bandwidthWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-traffic');
|
||||
const renderedWidget = renderDashboardWidget({
|
||||
|
||||
if (traffic.loading || traffic.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: traffic.loading,
|
||||
failed: traffic.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: traffic.loading,
|
||||
failed: traffic.failed,
|
||||
title: 'Traffic',
|
||||
items: [
|
||||
{ key: 'Uplink', value: `${prettyBytes(traffic.up)}/s` },
|
||||
{ key: 'Downlink', value: `${prettyBytes(traffic.down)}/s` },
|
||||
{ key: 'Uplink', value: `${prettyBytes(traffic.data.up)}/s` },
|
||||
{ key: 'Downlink', value: `${prettyBytes(traffic.data.down)}/s` },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -151,16 +228,34 @@ async function renderTrafficWidget() {
|
||||
}
|
||||
|
||||
async function renderTrafficTotalWidget() {
|
||||
const connections = store.get().connections;
|
||||
console.log('renderTrafficTotalWidget');
|
||||
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||
const renderedWidget = renderDashboardWidget({
|
||||
|
||||
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: trafficTotalWidget.loading,
|
||||
failed: trafficTotalWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: trafficTotalWidget.loading,
|
||||
failed: trafficTotalWidget.failed,
|
||||
title: 'Traffic Total',
|
||||
items: [
|
||||
{ key: 'Uplink', value: String(prettyBytes(connections.uploadTotal)) },
|
||||
{
|
||||
key: 'Uplink',
|
||||
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
|
||||
},
|
||||
{
|
||||
key: 'Downlink',
|
||||
value: String(prettyBytes(connections.downloadTotal)),
|
||||
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -169,44 +264,77 @@ async function renderTrafficTotalWidget() {
|
||||
}
|
||||
|
||||
async function renderSystemInfoWidget() {
|
||||
const connections = store.get().connections;
|
||||
console.log('renderSystemInfoWidget');
|
||||
const systemInfoWidget = store.get().systemInfoWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-system-info');
|
||||
const renderedWidget = renderDashboardWidget({
|
||||
|
||||
if (systemInfoWidget.loading || systemInfoWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: systemInfoWidget.loading,
|
||||
failed: systemInfoWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: systemInfoWidget.loading,
|
||||
failed: systemInfoWidget.failed,
|
||||
title: 'System info',
|
||||
items: [
|
||||
{
|
||||
key: 'Active Connections',
|
||||
value: String(connections.connections.length),
|
||||
value: String(systemInfoWidget.data.connections),
|
||||
},
|
||||
{
|
||||
key: 'Memory Usage',
|
||||
value: String(prettyBytes(systemInfoWidget.data.memory)),
|
||||
},
|
||||
{ key: 'Memory Usage', value: String(prettyBytes(connections.memory)) },
|
||||
],
|
||||
});
|
||||
|
||||
container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
async function renderServiceInfoWidget() {
|
||||
const services = store.get().services;
|
||||
async function renderServicesInfoWidget() {
|
||||
console.log('renderServicesInfoWidget');
|
||||
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-service-info');
|
||||
const renderedWidget = renderDashboardWidget({
|
||||
|
||||
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: servicesInfoWidget.loading,
|
||||
failed: servicesInfoWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: servicesInfoWidget.loading,
|
||||
failed: servicesInfoWidget.failed,
|
||||
title: 'Services info',
|
||||
items: [
|
||||
{
|
||||
key: 'Podkop',
|
||||
value: services.podkop ? '✔ Enabled' : '✘ Disabled',
|
||||
value: servicesInfoWidget.data.podkop ? '✔ Enabled' : '✘ Disabled',
|
||||
attributes: {
|
||||
class: services.podkop
|
||||
class: servicesInfoWidget.data.podkop
|
||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Sing-box',
|
||||
value: services.singbox ? '✔ Running' : '✘ Stopped',
|
||||
value: servicesInfoWidget.data.singbox ? '✔ Running' : '✘ Stopped',
|
||||
attributes: {
|
||||
class: services.singbox
|
||||
class: servicesInfoWidget.data.singbox
|
||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
@@ -222,21 +350,24 @@ async function onStoreUpdate(
|
||||
prev: StoreType,
|
||||
diff: Partial<StoreType>,
|
||||
) {
|
||||
if (diff?.dashboardSections) {
|
||||
renderDashboardSections();
|
||||
if (diff.sectionsWidget) {
|
||||
renderSectionsWidget();
|
||||
}
|
||||
|
||||
if (diff?.traffic) {
|
||||
renderTrafficWidget();
|
||||
if (diff.bandwidthWidget) {
|
||||
renderBandwidthWidget();
|
||||
}
|
||||
|
||||
if (diff?.connections) {
|
||||
if (diff.trafficTotalWidget) {
|
||||
renderTrafficTotalWidget();
|
||||
}
|
||||
|
||||
if (diff.systemInfoWidget) {
|
||||
renderSystemInfoWidget();
|
||||
}
|
||||
|
||||
if (diff?.services) {
|
||||
renderServiceInfoWidget();
|
||||
if (diff.servicesInfoWidget) {
|
||||
renderServicesInfoWidget();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { renderSections } from './renderSections';
|
||||
import { renderWidget } from './renderWidget';
|
||||
|
||||
export function renderDashboard() {
|
||||
return E(
|
||||
'div',
|
||||
@@ -8,59 +11,44 @@ export function renderDashboard() {
|
||||
[
|
||||
// Widgets section
|
||||
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
||||
E('div', { id: 'dashboard-widget-traffic' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
),
|
||||
]),
|
||||
E('div', { id: 'dashboard-widget-traffic-total' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
),
|
||||
]),
|
||||
E('div', { id: 'dashboard-widget-system-info' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
),
|
||||
]),
|
||||
E('div', { id: 'dashboard-widget-service-info' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
),
|
||||
]),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-traffic' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-traffic-total' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-system-info' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-service-info' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
]),
|
||||
// All outbounds
|
||||
E('div', { id: 'dashboard-sections-grid' }, [
|
||||
E('div', {
|
||||
id: 'dashboard-sections-grid-skeleton',
|
||||
class: 'pdk_dashboard-page__outbound-section skeleton',
|
||||
style: 'height: 127px',
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-sections-grid' },
|
||||
renderSections({
|
||||
loading: true,
|
||||
failed: false,
|
||||
section: {
|
||||
code: '',
|
||||
displayName: '',
|
||||
outbounds: [],
|
||||
withTagSelect: false,
|
||||
},
|
||||
onTestLatency: () => {},
|
||||
onChooseOutbound: () => {},
|
||||
}),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
import { Podkop } from '../../../types';
|
||||
import { Podkop } from '../../types';
|
||||
|
||||
interface IRenderOutboundGroupProps {
|
||||
interface IRenderSectionsProps {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
section: Podkop.OutboundGroup;
|
||||
onTestLatency: (tag: string) => void;
|
||||
onChooseOutbound: (selector: string, tag: string) => void;
|
||||
}
|
||||
|
||||
export function renderOutboundGroup({
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
class: 'pdk_dashboard-page__outbound-section centered',
|
||||
style: 'height: 127px',
|
||||
},
|
||||
E('span', {}, 'Dashboard currently unavailable'),
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoadingState() {
|
||||
return E('div', {
|
||||
id: 'dashboard-sections-grid-skeleton',
|
||||
class: 'pdk_dashboard-page__outbound-section skeleton',
|
||||
style: 'height: 127px',
|
||||
});
|
||||
}
|
||||
|
||||
export function renderDefaultState({
|
||||
section,
|
||||
onTestLatency,
|
||||
onChooseOutbound,
|
||||
}: IRenderOutboundGroupProps) {
|
||||
onTestLatency,
|
||||
}: IRenderSectionsProps) {
|
||||
function testLatency() {
|
||||
if (section.withTagSelect) {
|
||||
return onTestLatency(section.code);
|
||||
@@ -90,3 +111,15 @@ export function renderOutboundGroup({
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderSections(props: IRenderSectionsProps) {
|
||||
if (props.failed) {
|
||||
return renderFailedState();
|
||||
}
|
||||
|
||||
if (props.loading) {
|
||||
return renderLoadingState();
|
||||
}
|
||||
|
||||
return renderDefaultState(props);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
interface IRenderWidgetParams {
|
||||
interface IRenderWidgetProps {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
title: string;
|
||||
items: Array<{
|
||||
key: string;
|
||||
@@ -9,7 +11,31 @@ interface IRenderWidgetParams {
|
||||
}>;
|
||||
}
|
||||
|
||||
export function renderDashboardWidget({ title, items }: IRenderWidgetParams) {
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item centered',
|
||||
},
|
||||
'Currently unavailable',
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoadingState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
function renderDefaultState({ title, items }: IRenderWidgetProps) {
|
||||
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
|
||||
E(
|
||||
'b',
|
||||
@@ -38,3 +64,15 @@ export function renderDashboardWidget({ title, items }: IRenderWidgetParams) {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderWidget(props: IRenderWidgetProps) {
|
||||
if (props.loading) {
|
||||
return renderLoadingState();
|
||||
}
|
||||
|
||||
if (props.failed) {
|
||||
return renderFailedState();
|
||||
}
|
||||
|
||||
return renderDefaultState(props);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export function renderEmptyOutboundGroup() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
class: 'pdk_dashboard-page__outbound-section centered',
|
||||
style: 'height: 127px',
|
||||
},
|
||||
E('span', {}, 'Dashboard currently unavailable'),
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
// eslint-disable-next-line
|
||||
type Listener = (data: any) => void;
|
||||
type ErrorListener = (error: Event | string) => void;
|
||||
|
||||
class SocketManager {
|
||||
private static instance: SocketManager;
|
||||
private sockets = new Map<string, WebSocket>();
|
||||
private listeners = new Map<string, Set<Listener>>();
|
||||
private connected = new Map<string, boolean>();
|
||||
private errorListeners = new Map<string, Set<ErrorListener>>();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -23,9 +25,11 @@ class SocketManager {
|
||||
this.sockets.set(url, ws);
|
||||
this.connected.set(url, false);
|
||||
this.listeners.set(url, new Set());
|
||||
this.errorListeners.set(url, new Set());
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
this.connected.set(url, true);
|
||||
console.info(`Connected: ${url}`);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (event) => {
|
||||
@@ -43,23 +47,33 @@ class SocketManager {
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
this.connected.set(url, false);
|
||||
console.warn(`⚠️ Disconnected: ${url}`);
|
||||
console.warn(`Disconnected: ${url}`);
|
||||
this.triggerError(url, 'Connection closed');
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (err) => {
|
||||
console.error(`❌ Socket error for ${url}:`, err);
|
||||
console.error(`Socket error for ${url}:`, err);
|
||||
this.triggerError(url, err);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(url: string, listener: Listener): void {
|
||||
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||
if (!this.sockets.has(url)) {
|
||||
this.connect(url);
|
||||
}
|
||||
|
||||
this.listeners.get(url)?.add(listener);
|
||||
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.add(onError);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe(url: string, listener: Listener): void {
|
||||
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||
this.listeners.get(url)?.delete(listener);
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.delete(onError);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -68,7 +82,8 @@ class SocketManager {
|
||||
if (ws && this.connected.get(url)) {
|
||||
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||
} else {
|
||||
console.warn(`⚠️ Cannot send: not connected to ${url}`);
|
||||
console.warn(`Cannot send: not connected to ${url}`);
|
||||
this.triggerError(url, 'Not connected');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +93,7 @@ class SocketManager {
|
||||
ws.close();
|
||||
this.sockets.delete(url);
|
||||
this.listeners.delete(url);
|
||||
this.errorListeners.delete(url);
|
||||
this.connected.delete(url);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +103,19 @@ class SocketManager {
|
||||
this.disconnect(url);
|
||||
}
|
||||
}
|
||||
|
||||
private triggerError(url: string, err: Event | string): void {
|
||||
const handlers = this.errorListeners.get(url);
|
||||
if (handlers) {
|
||||
for (const cb of handlers) {
|
||||
try {
|
||||
cb(err);
|
||||
} catch (e) {
|
||||
console.error(`Error handler threw for ${url}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const socket = SocketManager.getInstance();
|
||||
|
||||
@@ -117,22 +117,30 @@ export interface StoreType {
|
||||
current: string;
|
||||
all: string[];
|
||||
};
|
||||
dashboardSections: {
|
||||
bandwidthWidget: {
|
||||
loading: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
failed: boolean;
|
||||
data: { up: number; down: number };
|
||||
};
|
||||
traffic: { up: number; down: number };
|
||||
memory: { inuse: number; oslimit: number };
|
||||
connections: {
|
||||
connections: unknown[];
|
||||
downloadTotal: number;
|
||||
memory: number;
|
||||
uploadTotal: number;
|
||||
trafficTotalWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { downloadTotal: number; uploadTotal: number };
|
||||
};
|
||||
services: {
|
||||
singbox: number;
|
||||
podkop: number;
|
||||
systemInfoWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { connections: number; memory: number };
|
||||
};
|
||||
servicesInfoWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { singbox: number; podkop: number };
|
||||
};
|
||||
sectionsWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,19 +149,31 @@ const initialStore: StoreType = {
|
||||
current: '',
|
||||
all: [],
|
||||
},
|
||||
dashboardSections: {
|
||||
data: [],
|
||||
bandwidthWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { up: 0, down: 0 },
|
||||
},
|
||||
traffic: { up: -1, down: -1 },
|
||||
memory: { inuse: -1, oslimit: -1 },
|
||||
connections: {
|
||||
connections: [],
|
||||
memory: -1,
|
||||
downloadTotal: -1,
|
||||
uploadTotal: -1,
|
||||
trafficTotalWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { connections: 0, memory: 0 },
|
||||
},
|
||||
servicesInfoWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { singbox: 0, podkop: 0 },
|
||||
},
|
||||
sectionsWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: [],
|
||||
},
|
||||
services: { singbox: -1, podkop: -1 },
|
||||
};
|
||||
|
||||
export const store = new Store<StoreType>(initialStore);
|
||||
|
||||
Reference in New Issue
Block a user