mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-12 14:37:03 +03:00
feat: implement dashboard tab
This commit is contained in:
@@ -6,5 +6,5 @@
|
|||||||
export * from './validators';
|
export * from './validators';
|
||||||
export * from './helpers';
|
export * from './helpers';
|
||||||
export * from './clash';
|
export * from './clash';
|
||||||
export * from './dashboard';
|
export * from './podkop';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
|||||||
3
fe-app-podkop/src/podkop/index.ts
Normal file
3
fe-app-podkop/src/podkop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './methods';
|
||||||
|
export * from './services';
|
||||||
|
export * from './tabs';
|
||||||
@@ -3,22 +3,30 @@ import { getConfigSections } from './getConfigSections';
|
|||||||
import { getClashProxies } from '../../clash';
|
import { getClashProxies } from '../../clash';
|
||||||
import { getProxyUrlName } from '../../helpers';
|
import { getProxyUrlName } from '../../helpers';
|
||||||
|
|
||||||
export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
|
interface IGetDashboardSectionsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Podkop.OutboundGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
||||||
const configSections = await getConfigSections();
|
const configSections = await getConfigSections();
|
||||||
const clashProxies = await getClashProxies();
|
const clashProxies = await getClashProxies();
|
||||||
|
|
||||||
const clashProxiesData = clashProxies.success
|
if (!clashProxies.success) {
|
||||||
? clashProxies.data
|
return {
|
||||||
: { proxies: [] };
|
success: false,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const proxies = Object.entries(clashProxiesData.proxies).map(
|
const proxies = Object.entries(clashProxies.data.proxies).map(
|
||||||
([key, value]) => ({
|
([key, value]) => ({
|
||||||
code: key,
|
code: key,
|
||||||
value,
|
value,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return configSections
|
const data = configSections
|
||||||
.filter((section) => section.mode !== 'block')
|
.filter((section) => section.mode !== 'block')
|
||||||
.map((section) => {
|
.map((section) => {
|
||||||
if (section.mode === 'proxy') {
|
if (section.mode === 'proxy') {
|
||||||
@@ -137,4 +145,9 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
|
|||||||
outbounds: [],
|
outbounds: [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { TabServiceInstance } from './tab.service';
|
||||||
|
import { store } from '../../store';
|
||||||
|
|
||||||
|
export function coreService() {
|
||||||
|
TabServiceInstance.onChange((activeId, tabs) => {
|
||||||
|
store.set({
|
||||||
|
tabService: {
|
||||||
|
current: activeId || '',
|
||||||
|
all: tabs.map((tab) => tab.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './tab.service';
|
||||||
|
export * from './core.service';
|
||||||
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
type TabInfo = {
|
||||||
|
el: HTMLElement;
|
||||||
|
id: string;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabChangeCallback = (activeId: string | null, allTabs: TabInfo[]) => void;
|
||||||
|
|
||||||
|
export class TabService {
|
||||||
|
private static instance: TabService;
|
||||||
|
private observer: MutationObserver | null = null;
|
||||||
|
private callback?: TabChangeCallback;
|
||||||
|
private lastActiveId: string | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): TabService {
|
||||||
|
if (!TabService.instance) {
|
||||||
|
TabService.instance = new TabService();
|
||||||
|
}
|
||||||
|
return TabService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
this.observer = new MutationObserver(() => this.handleMutations());
|
||||||
|
this.observer.observe(document.body, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial check
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMutations() {
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTabsInfo(): TabInfo[] {
|
||||||
|
const tabs = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.cbi-tab, .cbi-tab-disabled'),
|
||||||
|
);
|
||||||
|
return tabs.map((el) => ({
|
||||||
|
el,
|
||||||
|
id: el.dataset.tab || '',
|
||||||
|
active:
|
||||||
|
el.classList.contains('cbi-tab') &&
|
||||||
|
!el.classList.contains('cbi-tab-disabled'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActiveTabId(): string | null {
|
||||||
|
const active = document.querySelector<HTMLElement>(
|
||||||
|
'.cbi-tab:not(.cbi-tab-disabled)',
|
||||||
|
);
|
||||||
|
return active?.dataset.tab || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify() {
|
||||||
|
const tabs = this.getTabsInfo();
|
||||||
|
const activeId = this.getActiveTabId();
|
||||||
|
|
||||||
|
if (activeId !== this.lastActiveId) {
|
||||||
|
this.lastActiveId = activeId;
|
||||||
|
this.callback?.(activeId, tabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange(callback: TabChangeCallback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllTabs(): TabInfo[] {
|
||||||
|
return this.getTabsInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveTab(): string | null {
|
||||||
|
return this.getActiveTabId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect() {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabServiceInstance = TabService.getInstance();
|
||||||
@@ -2,33 +2,40 @@ import {
|
|||||||
getDashboardSections,
|
getDashboardSections,
|
||||||
getPodkopStatus,
|
getPodkopStatus,
|
||||||
getSingboxStatus,
|
getSingboxStatus,
|
||||||
} from '../podkop/methods';
|
} from '../../methods';
|
||||||
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
|
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
|
||||||
import { getClashWsUrl, onMount } from '../helpers';
|
import { getClashWsUrl, onMount } from '../../../helpers';
|
||||||
import { store } from '../store';
|
|
||||||
import { socket } from '../socket';
|
|
||||||
import { renderDashboardWidget } from './renderer/renderWidget';
|
import { renderDashboardWidget } from './renderer/renderWidget';
|
||||||
import { prettyBytes } from '../helpers/prettyBytes';
|
|
||||||
import {
|
import {
|
||||||
triggerLatencyGroupTest,
|
triggerLatencyGroupTest,
|
||||||
triggerLatencyProxyTest,
|
triggerLatencyProxyTest,
|
||||||
triggerProxySelector,
|
triggerProxySelector,
|
||||||
} from '../clash';
|
} from '../../../clash';
|
||||||
|
import { store, StoreType } from '../../../store';
|
||||||
|
import { socket } from '../../../socket';
|
||||||
|
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||||
|
import { renderEmptyOutboundGroup } from './renderer/renderEmptyOutboundGroup';
|
||||||
|
|
||||||
// Fetchers
|
// Fetchers
|
||||||
|
|
||||||
async function fetchDashboardSections() {
|
async function fetchDashboardSections() {
|
||||||
const sections = await getDashboardSections();
|
store.set({
|
||||||
|
dashboardSections: {
|
||||||
|
...store.get().dashboardSections,
|
||||||
|
failed: false,
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
store.set({ sections });
|
const { data, success } = await getDashboardSections();
|
||||||
|
|
||||||
|
store.set({ dashboardSections: { loading: false, data, failed: !success } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const podkop = await getPodkopStatus();
|
const podkop = await getPodkopStatus();
|
||||||
const singbox = await getSingboxStatus();
|
const singbox = await getSingboxStatus();
|
||||||
|
|
||||||
console.log('podkop', podkop);
|
|
||||||
console.log('singbox', singbox);
|
|
||||||
store.set({
|
store.set({
|
||||||
services: {
|
services: {
|
||||||
singbox: singbox.running,
|
singbox: singbox.running,
|
||||||
@@ -83,8 +90,10 @@ async function handleTestProxyLatency(tag: string) {
|
|||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceTestLatencyButtonsWithSkeleton () {
|
function replaceTestLatencyButtonsWithSkeleton() {
|
||||||
document.querySelectorAll('.dashboard-sections-grid-item-test-latency').forEach(el => {
|
document
|
||||||
|
.querySelectorAll('.dashboard-sections-grid-item-test-latency')
|
||||||
|
.forEach((el) => {
|
||||||
const newDiv = document.createElement('div');
|
const newDiv = document.createElement('div');
|
||||||
newDiv.className = 'skeleton';
|
newDiv.className = 'skeleton';
|
||||||
newDiv.style.width = '99px';
|
newDiv.style.width = '99px';
|
||||||
@@ -96,10 +105,16 @@ function replaceTestLatencyButtonsWithSkeleton () {
|
|||||||
// Renderer
|
// Renderer
|
||||||
|
|
||||||
async function renderDashboardSections() {
|
async function renderDashboardSections() {
|
||||||
const sections = store.get().sections;
|
const dashboardSections = store.get().dashboardSections;
|
||||||
console.log('render dashboard sections group');
|
|
||||||
const container = document.getElementById('dashboard-sections-grid');
|
const container = document.getElementById('dashboard-sections-grid');
|
||||||
const renderedOutboundGroups = sections.map((section) =>
|
|
||||||
|
if (dashboardSections.failed) {
|
||||||
|
const rendered = renderEmptyOutboundGroup();
|
||||||
|
|
||||||
|
return container!.replaceChildren(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedOutboundGroups = dashboardSections.data.map((section) =>
|
||||||
renderOutboundGroup({
|
renderOutboundGroup({
|
||||||
section,
|
section,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
@@ -122,7 +137,7 @@ async function renderDashboardSections() {
|
|||||||
|
|
||||||
async function renderTrafficWidget() {
|
async function renderTrafficWidget() {
|
||||||
const traffic = store.get().traffic;
|
const traffic = store.get().traffic;
|
||||||
console.log('render dashboard traffic widget');
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic');
|
const container = document.getElementById('dashboard-widget-traffic');
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: 'Traffic',
|
title: 'Traffic',
|
||||||
@@ -137,7 +152,7 @@ async function renderTrafficWidget() {
|
|||||||
|
|
||||||
async function renderTrafficTotalWidget() {
|
async function renderTrafficTotalWidget() {
|
||||||
const connections = store.get().connections;
|
const connections = store.get().connections;
|
||||||
console.log('render dashboard traffic total widget');
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic-total');
|
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: 'Traffic Total',
|
title: 'Traffic Total',
|
||||||
@@ -155,7 +170,7 @@ async function renderTrafficTotalWidget() {
|
|||||||
|
|
||||||
async function renderSystemInfoWidget() {
|
async function renderSystemInfoWidget() {
|
||||||
const connections = store.get().connections;
|
const connections = store.get().connections;
|
||||||
console.log('render dashboard system info widget');
|
|
||||||
const container = document.getElementById('dashboard-widget-system-info');
|
const container = document.getElementById('dashboard-widget-system-info');
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: 'System info',
|
title: 'System info',
|
||||||
@@ -173,7 +188,7 @@ async function renderSystemInfoWidget() {
|
|||||||
|
|
||||||
async function renderServiceInfoWidget() {
|
async function renderServiceInfoWidget() {
|
||||||
const services = store.get().services;
|
const services = store.get().services;
|
||||||
console.log('render dashboard service info widget');
|
|
||||||
const container = document.getElementById('dashboard-widget-service-info');
|
const container = document.getElementById('dashboard-widget-service-info');
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: 'Services info',
|
title: 'Services info',
|
||||||
@@ -202,12 +217,12 @@ async function renderServiceInfoWidget() {
|
|||||||
container!.replaceChildren(renderedWidget);
|
container!.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initDashboardController(): Promise<void> {
|
async function onStoreUpdate(
|
||||||
store.subscribe((next, prev, diff) => {
|
next: StoreType,
|
||||||
console.log('Store changed', { prev, next, diff });
|
prev: StoreType,
|
||||||
|
diff: Partial<StoreType>,
|
||||||
// Update sections render
|
) {
|
||||||
if (diff?.sections) {
|
if (diff?.dashboardSections) {
|
||||||
renderDashboardSections();
|
renderDashboardSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,10 +238,18 @@ export async function initDashboardController(): Promise<void> {
|
|||||||
if (diff?.services) {
|
if (diff?.services) {
|
||||||
renderServiceInfoWidget();
|
renderServiceInfoWidget();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export async function initDashboardController(): Promise<void> {
|
||||||
onMount('dashboard-status').then(() => {
|
onMount('dashboard-status').then(() => {
|
||||||
console.log('Mounting dashboard');
|
// Remove old listener
|
||||||
|
store.unsubscribe(onStoreUpdate);
|
||||||
|
// Clear store
|
||||||
|
store.reset();
|
||||||
|
|
||||||
|
// Add new listener
|
||||||
|
store.subscribe(onStoreUpdate);
|
||||||
|
|
||||||
// Initial sections fetch
|
// Initial sections fetch
|
||||||
fetchDashboardSections();
|
fetchDashboardSections();
|
||||||
fetchServicesInfo();
|
fetchServicesInfo();
|
||||||
@@ -6,18 +6,6 @@ export function renderDashboard() {
|
|||||||
class: 'pdk_dashboard-page',
|
class: 'pdk_dashboard-page',
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
// Title section
|
|
||||||
E('div', { class: 'pdk_dashboard-page__title-section' }, [
|
|
||||||
E(
|
|
||||||
'h3',
|
|
||||||
{ class: 'pdk_dashboard-page__title-section__title' },
|
|
||||||
'Overall (alpha)',
|
|
||||||
),
|
|
||||||
E('label', {}, [
|
|
||||||
E('input', { type: 'checkbox', disabled: true, checked: true }),
|
|
||||||
' Runtime',
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
// Widgets section
|
// Widgets section
|
||||||
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
||||||
E('div', { id: 'dashboard-widget-traffic' }, [
|
E('div', { id: 'dashboard-widget-traffic' }, [
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function renderEmptyOutboundGroup() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
|
style: 'height: 127px',
|
||||||
|
},
|
||||||
|
E('span', {}, 'Dashboard currently unavailable'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Podkop } from '../../podkop/types';
|
import { Podkop } from '../../../types';
|
||||||
|
|
||||||
interface IRenderOutboundGroupProps {
|
interface IRenderOutboundGroupProps {
|
||||||
section: Podkop.OutboundGroup;
|
section: Podkop.OutboundGroup;
|
||||||
@@ -74,7 +74,14 @@ export function renderOutboundGroup({
|
|||||||
},
|
},
|
||||||
section.displayName,
|
section.displayName,
|
||||||
),
|
),
|
||||||
E('button', { class: 'btn dashboard-sections-grid-item-test-latency', click: () => testLatency() }, 'Test latency'),
|
E(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||||
|
click: () => testLatency(),
|
||||||
|
},
|
||||||
|
'Test latency',
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dashboard';
|
||||||
@@ -26,7 +26,6 @@ class SocketManager {
|
|||||||
|
|
||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
this.connected.set(url, true);
|
this.connected.set(url, true);
|
||||||
console.log(`✅ Connected: ${url}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('message', (event) => {
|
ws.addEventListener('message', (event) => {
|
||||||
|
|||||||
@@ -1,14 +1,43 @@
|
|||||||
import { Podkop } from './podkop/types';
|
import { Podkop } from './podkop/types';
|
||||||
|
|
||||||
|
function jsonStableStringify<T, V>(obj: T): string {
|
||||||
|
return JSON.stringify(obj, (_, value) => {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key] = value[key];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, V>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonEqual<A, B>(a: A, b: B): boolean {
|
||||||
|
try {
|
||||||
|
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
class Store<T extends Record<string, any>> {
|
class Store<T extends Record<string, any>> {
|
||||||
private value: T;
|
private value: T;
|
||||||
|
private readonly initial: T;
|
||||||
private listeners = new Set<Listener<T>>();
|
private listeners = new Set<Listener<T>>();
|
||||||
|
private lastHash = '';
|
||||||
|
|
||||||
constructor(initial: T) {
|
constructor(initial: T) {
|
||||||
this.value = initial;
|
this.value = initial;
|
||||||
|
this.initial = structuredClone(initial);
|
||||||
|
this.lastHash = jsonStableStringify(initial);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): T {
|
get(): T {
|
||||||
@@ -17,14 +46,33 @@ class Store<T extends Record<string, any>> {
|
|||||||
|
|
||||||
set(next: Partial<T>): void {
|
set(next: Partial<T>): void {
|
||||||
const prev = this.value;
|
const prev = this.value;
|
||||||
const merged = { ...this.value, ...next };
|
const merged = { ...prev, ...next };
|
||||||
if (Object.is(prev, merged)) return;
|
|
||||||
|
if (jsonEqual(prev, merged)) return;
|
||||||
|
|
||||||
this.value = merged;
|
this.value = merged;
|
||||||
|
this.lastHash = jsonStableStringify(merged);
|
||||||
|
|
||||||
const diff: Partial<T> = {};
|
const diff: Partial<T> = {};
|
||||||
for (const key in merged) {
|
for (const key in merged) {
|
||||||
if (merged[key] !== prev[key]) diff[key] = merged[key];
|
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
const prev = this.value;
|
||||||
|
const next = structuredClone(this.initial);
|
||||||
|
|
||||||
|
if (jsonEqual(prev, next)) return;
|
||||||
|
|
||||||
|
this.value = next;
|
||||||
|
this.lastHash = jsonStableStringify(next);
|
||||||
|
|
||||||
|
const diff: Partial<T> = {};
|
||||||
|
for (const key in next) {
|
||||||
|
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
@@ -32,12 +80,16 @@ class Store<T extends Record<string, any>> {
|
|||||||
|
|
||||||
subscribe(cb: Listener<T>): () => void {
|
subscribe(cb: Listener<T>): () => void {
|
||||||
this.listeners.add(cb);
|
this.listeners.add(cb);
|
||||||
cb(this.value, this.value, {}); // первый вызов без diff
|
cb(this.value, this.value, {});
|
||||||
return () => this.listeners.delete(cb);
|
return () => this.listeners.delete(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsubscribe(cb: Listener<T>): void {
|
||||||
|
this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
patch<K extends keyof T>(key: K, value: T[K]): void {
|
patch<K extends keyof T>(key: K, value: T[K]): void {
|
||||||
this.set({ ...this.value, [key]: value });
|
this.set({ [key]: value } as unknown as Partial<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
getKey<K extends keyof T>(key: K): T[K] {
|
getKey<K extends keyof T>(key: K): T[K] {
|
||||||
@@ -49,18 +101,27 @@ class Store<T extends Record<string, any>> {
|
|||||||
cb: (value: T[K]) => void,
|
cb: (value: T[K]) => void,
|
||||||
): () => void {
|
): () => void {
|
||||||
let prev = this.value[key];
|
let prev = this.value[key];
|
||||||
const unsub = this.subscribe((val) => {
|
const wrapper: Listener<T> = (val) => {
|
||||||
if (val[key] !== prev) {
|
if (!jsonEqual(val[key], prev)) {
|
||||||
prev = val[key];
|
prev = val[key];
|
||||||
cb(val[key]);
|
cb(val[key]);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
return unsub;
|
this.listeners.add(wrapper);
|
||||||
|
return () => this.listeners.delete(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const store = new Store<{
|
export interface StoreType {
|
||||||
sections: Podkop.OutboundGroup[];
|
tabService: {
|
||||||
|
current: string;
|
||||||
|
all: string[];
|
||||||
|
};
|
||||||
|
dashboardSections: {
|
||||||
|
loading: boolean;
|
||||||
|
data: Podkop.OutboundGroup[];
|
||||||
|
failed: boolean;
|
||||||
|
};
|
||||||
traffic: { up: number; down: number };
|
traffic: { up: number; down: number };
|
||||||
memory: { inuse: number; oslimit: number };
|
memory: { inuse: number; oslimit: number };
|
||||||
connections: {
|
connections: {
|
||||||
@@ -73,10 +134,26 @@ export const store = new Store<{
|
|||||||
singbox: number;
|
singbox: number;
|
||||||
podkop: number;
|
podkop: number;
|
||||||
};
|
};
|
||||||
}>({
|
}
|
||||||
sections: [],
|
|
||||||
traffic: { up: 0, down: 0 },
|
const initialStore: StoreType = {
|
||||||
memory: { inuse: 0, oslimit: 0 },
|
tabService: {
|
||||||
connections: { connections: [], memory: 0, downloadTotal: 0, uploadTotal: 0 },
|
current: '',
|
||||||
|
all: [],
|
||||||
|
},
|
||||||
|
dashboardSections: {
|
||||||
|
data: [],
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
traffic: { up: -1, down: -1 },
|
||||||
|
memory: { inuse: -1, oslimit: -1 },
|
||||||
|
connections: {
|
||||||
|
connections: [],
|
||||||
|
memory: -1,
|
||||||
|
downloadTotal: -1,
|
||||||
|
uploadTotal: -1,
|
||||||
|
},
|
||||||
services: { singbox: -1, podkop: -1 },
|
services: { singbox: -1, podkop: -1 },
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const store = new Store<StoreType>(initialStore);
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ export const GlobalStyles = `
|
|||||||
color: var(--error-color-medium);
|
color: var(--error-color-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Skeleton styles*/
|
/* Skeleton styles*/
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background-color: var(--background-color-low, #e0e0e0);
|
background-color: var(--background-color-low, #e0e0e0);
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ function createDashboardSection(mainSection) {
|
|||||||
|
|
||||||
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
|
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
|
||||||
o.rawhtml = true;
|
o.rawhtml = true;
|
||||||
o.cfgvalue = () => main.renderDashboard();
|
o.cfgvalue = () => {
|
||||||
|
main.initDashboardController()
|
||||||
|
|
||||||
|
return main.renderDashboard()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntryPoint = {
|
const EntryPoint = {
|
||||||
|
|||||||
@@ -515,6 +515,12 @@ var GlobalStyles = `
|
|||||||
color: var(--error-color-medium);
|
color: var(--error-color-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Skeleton styles*/
|
/* Skeleton styles*/
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background-color: var(--background-color-low, #e0e0e0);
|
background-color: var(--background-color-low, #e0e0e0);
|
||||||
@@ -883,86 +889,6 @@ async function triggerLatencyProxyTest(tag, timeout = 2e3, url = "https://www.gs
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/dashboard/renderDashboard.ts
|
|
||||||
function renderDashboard() {
|
|
||||||
return E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "dashboard-status",
|
|
||||||
class: "pdk_dashboard-page"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
// Title section
|
|
||||||
E("div", { class: "pdk_dashboard-page__title-section" }, [
|
|
||||||
E(
|
|
||||||
"h3",
|
|
||||||
{ class: "pdk_dashboard-page__title-section__title" },
|
|
||||||
"Overall (alpha)"
|
|
||||||
),
|
|
||||||
E("label", {}, [
|
|
||||||
E("input", { type: "checkbox", disabled: true, checked: true }),
|
|
||||||
" Runtime"
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
// 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"
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
// 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"
|
|
||||||
})
|
|
||||||
])
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/podkop/methods/getConfigSections.ts
|
// src/podkop/methods/getConfigSections.ts
|
||||||
async function getConfigSections() {
|
async function getConfigSections() {
|
||||||
return uci.load("podkop").then(() => uci.sections("podkop"));
|
return uci.load("podkop").then(() => uci.sections("podkop"));
|
||||||
@@ -972,14 +898,19 @@ async function getConfigSections() {
|
|||||||
async function getDashboardSections() {
|
async function getDashboardSections() {
|
||||||
const configSections = await getConfigSections();
|
const configSections = await getConfigSections();
|
||||||
const clashProxies = await getClashProxies();
|
const clashProxies = await getClashProxies();
|
||||||
const clashProxiesData = clashProxies.success ? clashProxies.data : { proxies: [] };
|
if (!clashProxies.success) {
|
||||||
const proxies = Object.entries(clashProxiesData.proxies).map(
|
return {
|
||||||
|
success: false,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const proxies = Object.entries(clashProxies.data.proxies).map(
|
||||||
([key, value]) => ({
|
([key, value]) => ({
|
||||||
code: key,
|
code: key,
|
||||||
value
|
value
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return configSections.filter((section) => section.mode !== "block").map((section) => {
|
const data = configSections.filter((section) => section.mode !== "block").map((section) => {
|
||||||
if (section.mode === "proxy") {
|
if (section.mode === "proxy") {
|
||||||
if (section.proxy_config_type === "url") {
|
if (section.proxy_config_type === "url") {
|
||||||
const outbound = proxies.find(
|
const outbound = proxies.find(
|
||||||
@@ -1076,6 +1007,10 @@ async function getDashboardSections() {
|
|||||||
outbounds: []
|
outbounds: []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/podkop/methods/getPodkopStatus.ts
|
// src/podkop/methods/getPodkopStatus.ts
|
||||||
@@ -1104,7 +1039,258 @@ async function getSingboxStatus() {
|
|||||||
return { running: 0, enabled: 0, status: "unknown" };
|
return { running: 0, enabled: 0, status: "unknown" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/dashboard/renderer/renderOutboundGroup.ts
|
// src/podkop/services/tab.service.ts
|
||||||
|
var TabService = class _TabService {
|
||||||
|
constructor() {
|
||||||
|
this.observer = null;
|
||||||
|
this.lastActiveId = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
static getInstance() {
|
||||||
|
if (!_TabService.instance) {
|
||||||
|
_TabService.instance = new _TabService();
|
||||||
|
}
|
||||||
|
return _TabService.instance;
|
||||||
|
}
|
||||||
|
init() {
|
||||||
|
this.observer = new MutationObserver(() => this.handleMutations());
|
||||||
|
this.observer.observe(document.body, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
handleMutations() {
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
getTabsInfo() {
|
||||||
|
const tabs = Array.from(
|
||||||
|
document.querySelectorAll(".cbi-tab, .cbi-tab-disabled")
|
||||||
|
);
|
||||||
|
return tabs.map((el) => ({
|
||||||
|
el,
|
||||||
|
id: el.dataset.tab || "",
|
||||||
|
active: el.classList.contains("cbi-tab") && !el.classList.contains("cbi-tab-disabled")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
getActiveTabId() {
|
||||||
|
const active = document.querySelector(
|
||||||
|
".cbi-tab:not(.cbi-tab-disabled)"
|
||||||
|
);
|
||||||
|
return active?.dataset.tab || null;
|
||||||
|
}
|
||||||
|
notify() {
|
||||||
|
const tabs = this.getTabsInfo();
|
||||||
|
const activeId = this.getActiveTabId();
|
||||||
|
if (activeId !== this.lastActiveId) {
|
||||||
|
this.lastActiveId = activeId;
|
||||||
|
this.callback?.(activeId, tabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange(callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
getAllTabs() {
|
||||||
|
return this.getTabsInfo();
|
||||||
|
}
|
||||||
|
getActiveTab() {
|
||||||
|
return this.getActiveTabId();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var TabServiceInstance = TabService.getInstance();
|
||||||
|
|
||||||
|
// src/store.ts
|
||||||
|
function jsonStableStringify(obj) {
|
||||||
|
return JSON.stringify(obj, (_, value) => {
|
||||||
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
return Object.keys(value).sort().reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key] = value[key];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function jsonEqual(a, b) {
|
||||||
|
try {
|
||||||
|
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var Store = class {
|
||||||
|
constructor(initial) {
|
||||||
|
this.listeners = /* @__PURE__ */ new Set();
|
||||||
|
this.lastHash = "";
|
||||||
|
this.value = initial;
|
||||||
|
this.initial = structuredClone(initial);
|
||||||
|
this.lastHash = jsonStableStringify(initial);
|
||||||
|
}
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
set(next) {
|
||||||
|
const prev = this.value;
|
||||||
|
const merged = { ...prev, ...next };
|
||||||
|
if (jsonEqual(prev, merged)) return;
|
||||||
|
this.value = merged;
|
||||||
|
this.lastHash = jsonStableStringify(merged);
|
||||||
|
const diff = {};
|
||||||
|
for (const key in merged) {
|
||||||
|
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
||||||
|
}
|
||||||
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
|
}
|
||||||
|
reset() {
|
||||||
|
const prev = this.value;
|
||||||
|
const next = structuredClone(this.initial);
|
||||||
|
if (jsonEqual(prev, next)) return;
|
||||||
|
this.value = next;
|
||||||
|
this.lastHash = jsonStableStringify(next);
|
||||||
|
const diff = {};
|
||||||
|
for (const key in next) {
|
||||||
|
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
||||||
|
}
|
||||||
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
|
}
|
||||||
|
subscribe(cb) {
|
||||||
|
this.listeners.add(cb);
|
||||||
|
cb(this.value, this.value, {});
|
||||||
|
return () => this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
unsubscribe(cb) {
|
||||||
|
this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
patch(key, value) {
|
||||||
|
this.set({ [key]: value });
|
||||||
|
}
|
||||||
|
getKey(key) {
|
||||||
|
return this.value[key];
|
||||||
|
}
|
||||||
|
subscribeKey(key, cb) {
|
||||||
|
let prev = this.value[key];
|
||||||
|
const wrapper = (val) => {
|
||||||
|
if (!jsonEqual(val[key], prev)) {
|
||||||
|
prev = val[key];
|
||||||
|
cb(val[key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.listeners.add(wrapper);
|
||||||
|
return () => this.listeners.delete(wrapper);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var initialStore = {
|
||||||
|
tabService: {
|
||||||
|
current: "",
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
dashboardSections: {
|
||||||
|
data: [],
|
||||||
|
loading: true
|
||||||
|
},
|
||||||
|
traffic: { up: -1, down: -1 },
|
||||||
|
memory: { inuse: -1, oslimit: -1 },
|
||||||
|
connections: {
|
||||||
|
connections: [],
|
||||||
|
memory: -1,
|
||||||
|
downloadTotal: -1,
|
||||||
|
uploadTotal: -1
|
||||||
|
},
|
||||||
|
services: { singbox: -1, podkop: -1 }
|
||||||
|
};
|
||||||
|
var store = new Store(initialStore);
|
||||||
|
|
||||||
|
// src/podkop/services/core.service.ts
|
||||||
|
function coreService() {
|
||||||
|
TabServiceInstance.onChange((activeId, tabs) => {
|
||||||
|
store.set({
|
||||||
|
tabService: {
|
||||||
|
current: activeId || "",
|
||||||
|
all: tabs.map((tab) => tab.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/podkop/tabs/dashboard/renderDashboard.ts
|
||||||
|
function renderDashboard() {
|
||||||
|
return E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "dashboard-status",
|
||||||
|
class: "pdk_dashboard-page"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// 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"
|
||||||
|
},
|
||||||
|
""
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
// 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"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/podkop/tabs/dashboard/renderer/renderOutboundGroup.ts
|
||||||
function renderOutboundGroup({
|
function renderOutboundGroup({
|
||||||
section,
|
section,
|
||||||
onTestLatency,
|
onTestLatency,
|
||||||
@@ -1164,7 +1350,14 @@ function renderOutboundGroup({
|
|||||||
},
|
},
|
||||||
section.displayName
|
section.displayName
|
||||||
),
|
),
|
||||||
E("button", { class: "btn dashboard-sections-grid-item-test-latency", click: () => testLatency() }, "Test latency")
|
E(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "btn dashboard-sections-grid-item-test-latency",
|
||||||
|
click: () => testLatency()
|
||||||
|
},
|
||||||
|
"Test latency"
|
||||||
|
)
|
||||||
]),
|
]),
|
||||||
E(
|
E(
|
||||||
"div",
|
"div",
|
||||||
@@ -1174,55 +1367,36 @@ function renderOutboundGroup({
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/store.ts
|
// src/podkop/tabs/dashboard/renderer/renderWidget.ts
|
||||||
var Store = class {
|
function renderDashboardWidget({ title, items }) {
|
||||||
constructor(initial) {
|
return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [
|
||||||
this.listeners = /* @__PURE__ */ new Set();
|
E(
|
||||||
this.value = initial;
|
"b",
|
||||||
}
|
{ class: "pdk_dashboard-page__widgets-section__item__title" },
|
||||||
get() {
|
title
|
||||||
return this.value;
|
),
|
||||||
}
|
...items.map(
|
||||||
set(next) {
|
(item) => E(
|
||||||
const prev = this.value;
|
"div",
|
||||||
const merged = { ...this.value, ...next };
|
{
|
||||||
if (Object.is(prev, merged)) return;
|
class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}`
|
||||||
this.value = merged;
|
},
|
||||||
const diff = {};
|
[
|
||||||
for (const key in merged) {
|
E(
|
||||||
if (merged[key] !== prev[key]) diff[key] = merged[key];
|
"span",
|
||||||
}
|
{ class: "pdk_dashboard-page__widgets-section__item__row__key" },
|
||||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
`${item.key}: `
|
||||||
}
|
),
|
||||||
subscribe(cb) {
|
E(
|
||||||
this.listeners.add(cb);
|
"span",
|
||||||
cb(this.value, this.value, {});
|
{ class: "pdk_dashboard-page__widgets-section__item__row__value" },
|
||||||
return () => this.listeners.delete(cb);
|
item.value
|
||||||
}
|
)
|
||||||
patch(key, value) {
|
]
|
||||||
this.set({ ...this.value, [key]: value });
|
)
|
||||||
}
|
)
|
||||||
getKey(key) {
|
]);
|
||||||
return this.value[key];
|
}
|
||||||
}
|
|
||||||
subscribeKey(key, cb) {
|
|
||||||
let prev = this.value[key];
|
|
||||||
const unsub = this.subscribe((val) => {
|
|
||||||
if (val[key] !== prev) {
|
|
||||||
prev = val[key];
|
|
||||||
cb(val[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return unsub;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var store = new Store({
|
|
||||||
sections: [],
|
|
||||||
traffic: { up: 0, down: 0 },
|
|
||||||
memory: { inuse: 0, oslimit: 0 },
|
|
||||||
connections: { connections: [], memory: 0, downloadTotal: 0, uploadTotal: 0 },
|
|
||||||
services: { singbox: -1, podkop: -1 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// src/socket.ts
|
// src/socket.ts
|
||||||
var SocketManager = class _SocketManager {
|
var SocketManager = class _SocketManager {
|
||||||
@@ -1245,7 +1419,6 @@ var SocketManager = class _SocketManager {
|
|||||||
this.listeners.set(url, /* @__PURE__ */ new Set());
|
this.listeners.set(url, /* @__PURE__ */ new Set());
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
this.connected.set(url, true);
|
this.connected.set(url, true);
|
||||||
console.log(`\u2705 Connected: ${url}`);
|
|
||||||
});
|
});
|
||||||
ws.addEventListener("message", (event) => {
|
ws.addEventListener("message", (event) => {
|
||||||
const handlers = this.listeners.get(url);
|
const handlers = this.listeners.get(url);
|
||||||
@@ -1302,37 +1475,6 @@ var SocketManager = class _SocketManager {
|
|||||||
};
|
};
|
||||||
var socket = SocketManager.getInstance();
|
var socket = SocketManager.getInstance();
|
||||||
|
|
||||||
// src/dashboard/renderer/renderWidget.ts
|
|
||||||
function renderDashboardWidget({ title, items }) {
|
|
||||||
return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [
|
|
||||||
E(
|
|
||||||
"b",
|
|
||||||
{ class: "pdk_dashboard-page__widgets-section__item__title" },
|
|
||||||
title
|
|
||||||
),
|
|
||||||
...items.map(
|
|
||||||
(item) => E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}`
|
|
||||||
},
|
|
||||||
[
|
|
||||||
E(
|
|
||||||
"span",
|
|
||||||
{ class: "pdk_dashboard-page__widgets-section__item__row__key" },
|
|
||||||
`${item.key}: `
|
|
||||||
),
|
|
||||||
E(
|
|
||||||
"span",
|
|
||||||
{ class: "pdk_dashboard-page__widgets-section__item__row__value" },
|
|
||||||
item.value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/helpers/prettyBytes.ts
|
// src/helpers/prettyBytes.ts
|
||||||
function prettyBytes(n) {
|
function prettyBytes(n) {
|
||||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
@@ -1345,16 +1487,33 @@ function prettyBytes(n) {
|
|||||||
return n + " " + unit;
|
return n + " " + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/dashboard/initDashboardController.ts
|
// src/podkop/tabs/dashboard/renderer/renderEmptyOutboundGroup.ts
|
||||||
|
function renderEmptyOutboundGroup() {
|
||||||
|
return E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "pdk_dashboard-page__outbound-section centered",
|
||||||
|
style: "height: 127px"
|
||||||
|
},
|
||||||
|
E("span", {}, "Dashboard currently unavailable")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/podkop/tabs/dashboard/initDashboardController.ts
|
||||||
async function fetchDashboardSections() {
|
async function fetchDashboardSections() {
|
||||||
const sections = await getDashboardSections();
|
store.set({
|
||||||
store.set({ sections });
|
dashboardSections: {
|
||||||
|
...store.get().dashboardSections,
|
||||||
|
failed: false,
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { data, success } = await getDashboardSections();
|
||||||
|
store.set({ dashboardSections: { loading: false, data, failed: !success } });
|
||||||
}
|
}
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const podkop = await getPodkopStatus();
|
const podkop = await getPodkopStatus();
|
||||||
const singbox = await getSingboxStatus();
|
const singbox = await getSingboxStatus();
|
||||||
console.log("podkop", podkop);
|
|
||||||
console.log("singbox", singbox);
|
|
||||||
store.set({
|
store.set({
|
||||||
services: {
|
services: {
|
||||||
singbox: singbox.running,
|
singbox: singbox.running,
|
||||||
@@ -1408,10 +1567,13 @@ function replaceTestLatencyButtonsWithSkeleton() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function renderDashboardSections() {
|
async function renderDashboardSections() {
|
||||||
const sections = store.get().sections;
|
const dashboardSections = store.get().dashboardSections;
|
||||||
console.log("render dashboard sections group");
|
|
||||||
const container = document.getElementById("dashboard-sections-grid");
|
const container = document.getElementById("dashboard-sections-grid");
|
||||||
const renderedOutboundGroups = sections.map(
|
if (dashboardSections.failed) {
|
||||||
|
const rendered = renderEmptyOutboundGroup();
|
||||||
|
return container.replaceChildren(rendered);
|
||||||
|
}
|
||||||
|
const renderedOutboundGroups = dashboardSections.data.map(
|
||||||
(section) => renderOutboundGroup({
|
(section) => renderOutboundGroup({
|
||||||
section,
|
section,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
@@ -1430,7 +1592,6 @@ async function renderDashboardSections() {
|
|||||||
}
|
}
|
||||||
async function renderTrafficWidget() {
|
async function renderTrafficWidget() {
|
||||||
const traffic = store.get().traffic;
|
const traffic = store.get().traffic;
|
||||||
console.log("render dashboard traffic widget");
|
|
||||||
const container = document.getElementById("dashboard-widget-traffic");
|
const container = document.getElementById("dashboard-widget-traffic");
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: "Traffic",
|
title: "Traffic",
|
||||||
@@ -1443,7 +1604,6 @@ async function renderTrafficWidget() {
|
|||||||
}
|
}
|
||||||
async function renderTrafficTotalWidget() {
|
async function renderTrafficTotalWidget() {
|
||||||
const connections = store.get().connections;
|
const connections = store.get().connections;
|
||||||
console.log("render dashboard traffic total widget");
|
|
||||||
const container = document.getElementById("dashboard-widget-traffic-total");
|
const container = document.getElementById("dashboard-widget-traffic-total");
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: "Traffic Total",
|
title: "Traffic Total",
|
||||||
@@ -1459,7 +1619,6 @@ async function renderTrafficTotalWidget() {
|
|||||||
}
|
}
|
||||||
async function renderSystemInfoWidget() {
|
async function renderSystemInfoWidget() {
|
||||||
const connections = store.get().connections;
|
const connections = store.get().connections;
|
||||||
console.log("render dashboard system info widget");
|
|
||||||
const container = document.getElementById("dashboard-widget-system-info");
|
const container = document.getElementById("dashboard-widget-system-info");
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: "System info",
|
title: "System info",
|
||||||
@@ -1475,7 +1634,6 @@ async function renderSystemInfoWidget() {
|
|||||||
}
|
}
|
||||||
async function renderServiceInfoWidget() {
|
async function renderServiceInfoWidget() {
|
||||||
const services = store.get().services;
|
const services = store.get().services;
|
||||||
console.log("render dashboard service info widget");
|
|
||||||
const container = document.getElementById("dashboard-widget-service-info");
|
const container = document.getElementById("dashboard-widget-service-info");
|
||||||
const renderedWidget = renderDashboardWidget({
|
const renderedWidget = renderDashboardWidget({
|
||||||
title: "Services info",
|
title: "Services info",
|
||||||
@@ -1498,10 +1656,8 @@ async function renderServiceInfoWidget() {
|
|||||||
});
|
});
|
||||||
container.replaceChildren(renderedWidget);
|
container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
async function initDashboardController() {
|
async function onStoreUpdate(next, prev, diff) {
|
||||||
store.subscribe((next, prev, diff) => {
|
if (diff?.dashboardSections) {
|
||||||
console.log("Store changed", { prev, next, diff });
|
|
||||||
if (diff?.sections) {
|
|
||||||
renderDashboardSections();
|
renderDashboardSections();
|
||||||
}
|
}
|
||||||
if (diff?.traffic) {
|
if (diff?.traffic) {
|
||||||
@@ -1514,9 +1670,12 @@ async function initDashboardController() {
|
|||||||
if (diff?.services) {
|
if (diff?.services) {
|
||||||
renderServiceInfoWidget();
|
renderServiceInfoWidget();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
async function initDashboardController() {
|
||||||
onMount("dashboard-status").then(() => {
|
onMount("dashboard-status").then(() => {
|
||||||
console.log("Mounting dashboard");
|
store.unsubscribe(onStoreUpdate);
|
||||||
|
store.reset();
|
||||||
|
store.subscribe(onStoreUpdate);
|
||||||
fetchDashboardSections();
|
fetchDashboardSections();
|
||||||
fetchServicesInfo();
|
fetchServicesInfo();
|
||||||
connectToClashSockets();
|
connectToClashSockets();
|
||||||
@@ -1539,9 +1698,12 @@ return baseclass.extend({
|
|||||||
IP_CHECK_DOMAIN,
|
IP_CHECK_DOMAIN,
|
||||||
REGIONAL_OPTIONS,
|
REGIONAL_OPTIONS,
|
||||||
STATUS_COLORS,
|
STATUS_COLORS,
|
||||||
|
TabService,
|
||||||
|
TabServiceInstance,
|
||||||
UPDATE_INTERVAL_OPTIONS,
|
UPDATE_INTERVAL_OPTIONS,
|
||||||
bulkValidate,
|
bulkValidate,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
coreService,
|
||||||
createBaseApiRequest,
|
createBaseApiRequest,
|
||||||
executeShellCommand,
|
executeShellCommand,
|
||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
@@ -1551,7 +1713,11 @@ return baseclass.extend({
|
|||||||
getClashProxies,
|
getClashProxies,
|
||||||
getClashVersion,
|
getClashVersion,
|
||||||
getClashWsUrl,
|
getClashWsUrl,
|
||||||
|
getConfigSections,
|
||||||
|
getDashboardSections,
|
||||||
|
getPodkopStatus,
|
||||||
getProxyUrlName,
|
getProxyUrlName,
|
||||||
|
getSingboxStatus,
|
||||||
initDashboardController,
|
initDashboardController,
|
||||||
injectGlobalStyles,
|
injectGlobalStyles,
|
||||||
maskIP,
|
maskIP,
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ const EntryNode = {
|
|||||||
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
|
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
|
||||||
mainSection.anonymous = true;
|
mainSection.anonymous = true;
|
||||||
|
|
||||||
dashboardTab.createDashboardSection(mainSection);
|
|
||||||
|
|
||||||
main.initDashboardController();
|
|
||||||
|
|
||||||
configSection.createConfigSection(mainSection);
|
configSection.createConfigSection(mainSection);
|
||||||
|
|
||||||
// Additional Settings Tab (main section)
|
// Additional Settings Tab (main section)
|
||||||
@@ -84,6 +80,16 @@ const EntryNode = {
|
|||||||
extraSection.multiple = true;
|
extraSection.multiple = true;
|
||||||
configSection.createConfigSection(extraSection);
|
configSection.createConfigSection(extraSection);
|
||||||
|
|
||||||
|
|
||||||
|
// Initial dashboard render
|
||||||
|
dashboardTab.createDashboardSection(mainSection);
|
||||||
|
|
||||||
|
// Inject dashboard actualizer logic
|
||||||
|
// main.initDashboardController();
|
||||||
|
|
||||||
|
// Inject core service
|
||||||
|
main.coreService();
|
||||||
|
|
||||||
return podkopFormMapPromise;
|
return podkopFormMapPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user