feat: implement dashboard tab

This commit is contained in:
divocat
2025-10-07 00:36:36 +03:00
parent 1e4cda9400
commit 7cb43ffb65
19 changed files with 697 additions and 287 deletions

View File

@@ -6,5 +6,5 @@
export * from './validators';
export * from './helpers';
export * from './clash';
export * from './dashboard';
export * from './podkop';
export * from './constants';

View File

@@ -0,0 +1,3 @@
export * from './methods';
export * from './services';
export * from './tabs';

View File

@@ -3,22 +3,30 @@ import { getConfigSections } from './getConfigSections';
import { getClashProxies } from '../../clash';
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 clashProxies = await getClashProxies();
const clashProxiesData = clashProxies.success
? clashProxies.data
: { proxies: [] };
if (!clashProxies.success) {
return {
success: false,
data: [],
};
}
const proxies = Object.entries(clashProxiesData.proxies).map(
const proxies = Object.entries(clashProxies.data.proxies).map(
([key, value]) => ({
code: key,
value,
}),
);
return configSections
const data = configSections
.filter((section) => section.mode !== 'block')
.map((section) => {
if (section.mode === 'proxy') {
@@ -137,4 +145,9 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
outbounds: [],
};
});
return {
success: true,
data,
};
}

View 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),
},
});
});
}

View File

@@ -0,0 +1,2 @@
export * from './tab.service';
export * from './core.service';

View 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();

View File

@@ -2,33 +2,40 @@ import {
getDashboardSections,
getPodkopStatus,
getSingboxStatus,
} from '../podkop/methods';
} from '../../methods';
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
import { getClashWsUrl, onMount } from '../helpers';
import { store } from '../store';
import { socket } from '../socket';
import { getClashWsUrl, onMount } from '../../../helpers';
import { renderDashboardWidget } from './renderer/renderWidget';
import { prettyBytes } from '../helpers/prettyBytes';
import {
triggerLatencyGroupTest,
triggerLatencyProxyTest,
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
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() {
const podkop = await getPodkopStatus();
const singbox = await getSingboxStatus();
console.log('podkop', podkop);
console.log('singbox', singbox);
store.set({
services: {
singbox: singbox.running,
@@ -84,7 +91,9 @@ async function handleTestProxyLatency(tag: string) {
}
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');
newDiv.className = 'skeleton';
newDiv.style.width = '99px';
@@ -96,10 +105,16 @@ function replaceTestLatencyButtonsWithSkeleton () {
// Renderer
async function renderDashboardSections() {
const sections = store.get().sections;
console.log('render dashboard sections group');
const dashboardSections = store.get().dashboardSections;
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({
section,
onTestLatency: (tag) => {
@@ -122,7 +137,7 @@ async function renderDashboardSections() {
async function renderTrafficWidget() {
const traffic = store.get().traffic;
console.log('render dashboard traffic widget');
const container = document.getElementById('dashboard-widget-traffic');
const renderedWidget = renderDashboardWidget({
title: 'Traffic',
@@ -137,7 +152,7 @@ async function renderTrafficWidget() {
async function renderTrafficTotalWidget() {
const connections = store.get().connections;
console.log('render dashboard traffic total widget');
const container = document.getElementById('dashboard-widget-traffic-total');
const renderedWidget = renderDashboardWidget({
title: 'Traffic Total',
@@ -155,7 +170,7 @@ async function renderTrafficTotalWidget() {
async function renderSystemInfoWidget() {
const connections = store.get().connections;
console.log('render dashboard system info widget');
const container = document.getElementById('dashboard-widget-system-info');
const renderedWidget = renderDashboardWidget({
title: 'System info',
@@ -173,7 +188,7 @@ async function renderSystemInfoWidget() {
async function renderServiceInfoWidget() {
const services = store.get().services;
console.log('render dashboard service info widget');
const container = document.getElementById('dashboard-widget-service-info');
const renderedWidget = renderDashboardWidget({
title: 'Services info',
@@ -202,12 +217,12 @@ async function renderServiceInfoWidget() {
container!.replaceChildren(renderedWidget);
}
export async function initDashboardController(): Promise<void> {
store.subscribe((next, prev, diff) => {
console.log('Store changed', { prev, next, diff });
// Update sections render
if (diff?.sections) {
async function onStoreUpdate(
next: StoreType,
prev: StoreType,
diff: Partial<StoreType>,
) {
if (diff?.dashboardSections) {
renderDashboardSections();
}
@@ -223,10 +238,18 @@ export async function initDashboardController(): Promise<void> {
if (diff?.services) {
renderServiceInfoWidget();
}
});
}
export async function initDashboardController(): Promise<void> {
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
fetchDashboardSections();
fetchServicesInfo();

View File

@@ -6,18 +6,6 @@ export function renderDashboard() {
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' }, [

View File

@@ -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'),
);
}

View File

@@ -1,4 +1,4 @@
import { Podkop } from '../../podkop/types';
import { Podkop } from '../../../types';
interface IRenderOutboundGroupProps {
section: Podkop.OutboundGroup;
@@ -74,7 +74,14 @@ export function renderOutboundGroup({
},
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(
'div',

View File

@@ -0,0 +1 @@
export * from './dashboard';

View File

@@ -26,7 +26,6 @@ class SocketManager {
ws.addEventListener('open', () => {
this.connected.set(url, true);
console.log(`✅ Connected: ${url}`);
});
ws.addEventListener('message', (event) => {

View File

@@ -1,14 +1,43 @@
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;
// eslint-disable-next-line
class Store<T extends Record<string, any>> {
private value: T;
private readonly initial: T;
private listeners = new Set<Listener<T>>();
private lastHash = '';
constructor(initial: T) {
this.value = initial;
this.initial = structuredClone(initial);
this.lastHash = jsonStableStringify(initial);
}
get(): T {
@@ -17,14 +46,33 @@ class Store<T extends Record<string, any>> {
set(next: Partial<T>): void {
const prev = this.value;
const merged = { ...this.value, ...next };
if (Object.is(prev, merged)) return;
const merged = { ...prev, ...next };
if (jsonEqual(prev, merged)) return;
this.value = merged;
this.lastHash = jsonStableStringify(merged);
const diff: Partial<T> = {};
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));
@@ -32,12 +80,16 @@ class Store<T extends Record<string, any>> {
subscribe(cb: Listener<T>): () => void {
this.listeners.add(cb);
cb(this.value, this.value, {}); // первый вызов без diff
cb(this.value, this.value, {});
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 {
this.set({ ...this.value, [key]: value });
this.set({ [key]: value } as unknown as Partial<T>);
}
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,
): () => void {
let prev = this.value[key];
const unsub = this.subscribe((val) => {
if (val[key] !== prev) {
const wrapper: Listener<T> = (val) => {
if (!jsonEqual(val[key], prev)) {
prev = val[key];
cb(val[key]);
}
});
return unsub;
};
this.listeners.add(wrapper);
return () => this.listeners.delete(wrapper);
}
}
export const store = new Store<{
sections: Podkop.OutboundGroup[];
export interface StoreType {
tabService: {
current: string;
all: string[];
};
dashboardSections: {
loading: boolean;
data: Podkop.OutboundGroup[];
failed: boolean;
};
traffic: { up: number; down: number };
memory: { inuse: number; oslimit: number };
connections: {
@@ -73,10 +134,26 @@ export const store = new Store<{
singbox: number;
podkop: number;
};
}>({
sections: [],
traffic: { up: 0, down: 0 },
memory: { inuse: 0, oslimit: 0 },
connections: { connections: [], memory: 0, downloadTotal: 0, uploadTotal: 0 },
}
const initialStore: StoreType = {
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 },
});
};
export const store = new Store<StoreType>(initialStore);

View File

@@ -167,6 +167,12 @@ export const GlobalStyles = `
color: var(--error-color-medium);
}
.centered {
display: flex;
align-items: center;
justify-content: center;
}
/* Skeleton styles*/
.skeleton {
background-color: var(--background-color-low, #e0e0e0);

View File

@@ -12,7 +12,11 @@ function createDashboardSection(mainSection) {
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => main.renderDashboard();
o.cfgvalue = () => {
main.initDashboardController()
return main.renderDashboard()
};
}
const EntryPoint = {

View File

@@ -515,6 +515,12 @@ var GlobalStyles = `
color: var(--error-color-medium);
}
.centered {
display: flex;
align-items: center;
justify-content: center;
}
/* Skeleton styles*/
.skeleton {
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
async function getConfigSections() {
return uci.load("podkop").then(() => uci.sections("podkop"));
@@ -972,14 +898,19 @@ async function getConfigSections() {
async function getDashboardSections() {
const configSections = await getConfigSections();
const clashProxies = await getClashProxies();
const clashProxiesData = clashProxies.success ? clashProxies.data : { proxies: [] };
const proxies = Object.entries(clashProxiesData.proxies).map(
if (!clashProxies.success) {
return {
success: false,
data: []
};
}
const proxies = Object.entries(clashProxies.data.proxies).map(
([key, value]) => ({
code: key,
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.proxy_config_type === "url") {
const outbound = proxies.find(
@@ -1076,6 +1007,10 @@ async function getDashboardSections() {
outbounds: []
};
});
return {
success: true,
data
};
}
// src/podkop/methods/getPodkopStatus.ts
@@ -1104,7 +1039,258 @@ async function getSingboxStatus() {
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({
section,
onTestLatency,
@@ -1164,7 +1350,14 @@ function renderOutboundGroup({
},
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(
"div",
@@ -1174,55 +1367,36 @@ function renderOutboundGroup({
]);
}
// src/store.ts
var Store = class {
constructor(initial) {
this.listeners = /* @__PURE__ */ new Set();
this.value = initial;
// src/podkop/tabs/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
)
]
)
)
]);
}
get() {
return this.value;
}
set(next) {
const prev = this.value;
const merged = { ...this.value, ...next };
if (Object.is(prev, merged)) return;
this.value = merged;
const diff = {};
for (const key in merged) {
if (merged[key] !== prev[key]) diff[key] = merged[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);
}
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
var SocketManager = class _SocketManager {
@@ -1245,7 +1419,6 @@ var SocketManager = class _SocketManager {
this.listeners.set(url, /* @__PURE__ */ new Set());
ws.addEventListener("open", () => {
this.connected.set(url, true);
console.log(`\u2705 Connected: ${url}`);
});
ws.addEventListener("message", (event) => {
const handlers = this.listeners.get(url);
@@ -1302,37 +1475,6 @@ var SocketManager = class _SocketManager {
};
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
function prettyBytes(n) {
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
@@ -1345,16 +1487,33 @@ function prettyBytes(n) {
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() {
const sections = await getDashboardSections();
store.set({ sections });
store.set({
dashboardSections: {
...store.get().dashboardSections,
failed: false,
loading: true
}
});
const { data, success } = await getDashboardSections();
store.set({ dashboardSections: { loading: false, data, failed: !success } });
}
async function fetchServicesInfo() {
const podkop = await getPodkopStatus();
const singbox = await getSingboxStatus();
console.log("podkop", podkop);
console.log("singbox", singbox);
store.set({
services: {
singbox: singbox.running,
@@ -1408,10 +1567,13 @@ function replaceTestLatencyButtonsWithSkeleton() {
});
}
async function renderDashboardSections() {
const sections = store.get().sections;
console.log("render dashboard sections group");
const dashboardSections = store.get().dashboardSections;
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,
onTestLatency: (tag) => {
@@ -1430,7 +1592,6 @@ async function renderDashboardSections() {
}
async function renderTrafficWidget() {
const traffic = store.get().traffic;
console.log("render dashboard traffic widget");
const container = document.getElementById("dashboard-widget-traffic");
const renderedWidget = renderDashboardWidget({
title: "Traffic",
@@ -1443,7 +1604,6 @@ async function renderTrafficWidget() {
}
async function renderTrafficTotalWidget() {
const connections = store.get().connections;
console.log("render dashboard traffic total widget");
const container = document.getElementById("dashboard-widget-traffic-total");
const renderedWidget = renderDashboardWidget({
title: "Traffic Total",
@@ -1459,7 +1619,6 @@ async function renderTrafficTotalWidget() {
}
async function renderSystemInfoWidget() {
const connections = store.get().connections;
console.log("render dashboard system info widget");
const container = document.getElementById("dashboard-widget-system-info");
const renderedWidget = renderDashboardWidget({
title: "System info",
@@ -1475,7 +1634,6 @@ async function renderSystemInfoWidget() {
}
async function renderServiceInfoWidget() {
const services = store.get().services;
console.log("render dashboard service info widget");
const container = document.getElementById("dashboard-widget-service-info");
const renderedWidget = renderDashboardWidget({
title: "Services info",
@@ -1498,10 +1656,8 @@ async function renderServiceInfoWidget() {
});
container.replaceChildren(renderedWidget);
}
async function initDashboardController() {
store.subscribe((next, prev, diff) => {
console.log("Store changed", { prev, next, diff });
if (diff?.sections) {
async function onStoreUpdate(next, prev, diff) {
if (diff?.dashboardSections) {
renderDashboardSections();
}
if (diff?.traffic) {
@@ -1514,9 +1670,12 @@ async function initDashboardController() {
if (diff?.services) {
renderServiceInfoWidget();
}
});
}
async function initDashboardController() {
onMount("dashboard-status").then(() => {
console.log("Mounting dashboard");
store.unsubscribe(onStoreUpdate);
store.reset();
store.subscribe(onStoreUpdate);
fetchDashboardSections();
fetchServicesInfo();
connectToClashSockets();
@@ -1539,9 +1698,12 @@ return baseclass.extend({
IP_CHECK_DOMAIN,
REGIONAL_OPTIONS,
STATUS_COLORS,
TabService,
TabServiceInstance,
UPDATE_INTERVAL_OPTIONS,
bulkValidate,
copyToClipboard,
coreService,
createBaseApiRequest,
executeShellCommand,
getBaseUrl,
@@ -1551,7 +1713,11 @@ return baseclass.extend({
getClashProxies,
getClashVersion,
getClashWsUrl,
getConfigSections,
getDashboardSections,
getPodkopStatus,
getProxyUrlName,
getSingboxStatus,
initDashboardController,
injectGlobalStyles,
maskIP,

View File

@@ -34,10 +34,6 @@ const EntryNode = {
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
mainSection.anonymous = true;
dashboardTab.createDashboardSection(mainSection);
main.initDashboardController();
configSection.createConfigSection(mainSection);
// Additional Settings Tab (main section)
@@ -84,6 +80,16 @@ const EntryNode = {
extraSection.multiple = true;
configSection.createConfigSection(extraSection);
// Initial dashboard render
dashboardTab.createDashboardSection(mainSection);
// Inject dashboard actualizer logic
// main.initDashboardController();
// Inject core service
main.coreService();
return podkopFormMapPromise;
}
}