feat: implement dashboard prototype

This commit is contained in:
divocat
2025-10-06 03:43:55 +03:00
parent c75dd3e78b
commit aad6d8c002
35 changed files with 2014 additions and 26 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea .idea
fe-app-podkop/node_modules fe-app-podkop/node_modules
fe-app-podkop/.env

View File

@@ -7,7 +7,7 @@ export default [
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
{ {
ignores: ['node_modules'], ignores: ['node_modules', 'watch-upload.js'],
}, },
{ {
rules: { rules: {

View File

@@ -10,14 +10,19 @@
"build": "tsup src/main.ts", "build": "tsup src/main.ts",
"dev": "tsup src/main.ts --watch", "dev": "tsup src/main.ts --watch",
"test": "vitest", "test": "vitest",
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build" "ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
"watch:sftp": "node watch-upload.js"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0", "@typescript-eslint/parser": "8.45.0",
"chokidar": "4.0.3",
"dotenv": "17.2.3",
"eslint": "9.36.0", "eslint": "9.36.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"glob": "11.0.3",
"prettier": "3.6.2", "prettier": "3.6.2",
"ssh2-sftp-client": "12.0.1",
"tsup": "8.5.0", "tsup": "8.5.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.45.0", "typescript-eslint": "8.45.0",

View File

@@ -1,11 +1,12 @@
import { ClashAPI, IBaseApiResponse } from '../types'; import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest'; import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashConfig(): Promise< export async function getClashConfig(): Promise<
IBaseApiResponse<ClashAPI.Config> IBaseApiResponse<ClashAPI.Config>
> { > {
return createBaseApiRequest<ClashAPI.Config>(() => return createBaseApiRequest<ClashAPI.Config>(() =>
fetch('http://192.168.160.129:9090/configs', { fetch(`${getClashApiUrl()}/configs`, {
method: 'GET', method: 'GET',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}), }),

View File

@@ -1,12 +1,13 @@
import { ClashAPI, IBaseApiResponse } from '../types'; import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest'; import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashGroupDelay( export async function getClashGroupDelay(
group: string, group: string,
url = 'https://www.gstatic.com/generate_204', url = 'https://www.gstatic.com/generate_204',
timeout = 2000, timeout = 2000,
): Promise<IBaseApiResponse<ClashAPI.Delays>> { ): Promise<IBaseApiResponse<ClashAPI.Delays>> {
const endpoint = `http://192.168.160.129:9090/group/${group}/delay?url=${encodeURIComponent( const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
url, url,
)}&timeout=${timeout}`; )}&timeout=${timeout}`;

View File

@@ -1,11 +1,12 @@
import { ClashAPI, IBaseApiResponse } from '../types'; import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest'; import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashProxies(): Promise< export async function getClashProxies(): Promise<
IBaseApiResponse<ClashAPI.Proxies> IBaseApiResponse<ClashAPI.Proxies>
> { > {
return createBaseApiRequest<ClashAPI.Proxies>(() => return createBaseApiRequest<ClashAPI.Proxies>(() =>
fetch('http://192.168.160.129:9090/proxies', { fetch(`${getClashApiUrl()}/proxies`, {
method: 'GET', method: 'GET',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}), }),

View File

@@ -1,11 +1,12 @@
import { ClashAPI, IBaseApiResponse } from '../types'; import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest'; import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashVersion(): Promise< export async function getClashVersion(): Promise<
IBaseApiResponse<ClashAPI.Version> IBaseApiResponse<ClashAPI.Version>
> { > {
return createBaseApiRequest<ClashAPI.Version>(() => return createBaseApiRequest<ClashAPI.Version>(() =>
fetch('http://192.168.160.129:9090/version', { fetch(`${getClashApiUrl()}/version`, {
method: 'GET', method: 'GET',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}), }),

View File

@@ -3,3 +3,4 @@ export * from './getConfig';
export * from './getGroupDelay'; export * from './getGroupDelay';
export * from './getProxies'; export * from './getProxies';
export * from './getVersion'; export * from './getVersion';
export * from './triggerProxySelector';

View File

@@ -0,0 +1,16 @@
import { IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function triggerProxySelector(
selector: string,
outbound: string,
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(`${getClashApiUrl()}/proxies/${selector}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: outbound }),
}),
);
}

View File

@@ -0,0 +1,2 @@
export * from './renderDashboard';
export * from './initDashboardController';

View File

@@ -0,0 +1,174 @@
import {
getDashboardSections,
getPodkopStatus,
getSingboxStatus,
} from '../podkop/methods';
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
import { getClashWsUrl, onMount } from '../helpers';
import { store } from '../store';
import { socket } from '../socket';
import { renderDashboardWidget } from './renderer/renderWidget';
import { prettyBytes } from '../helpers/prettyBytes';
// Fetchers
async function fetchDashboardSections() {
const sections = await getDashboardSections();
store.set({ sections });
}
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 ? '✔ Enabled' : singbox.status,
podkop: podkop.status ? '✔ Enabled' : podkop.status,
},
});
}
async function connectToClashSockets() {
socket.subscribe(`${getClashWsUrl()}/traffic?token=`, (msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
traffic: { up: parsedMsg.up, down: parsedMsg.down },
});
});
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 },
});
});
}
// Renderer
async function renderDashboardSections() {
const sections = store.get().sections;
console.log('render dashboard sections group');
const container = document.getElementById('dashboard-sections-grid');
const renderedOutboundGroups = sections.map(renderOutboundGroup);
container!.replaceChildren(...renderedOutboundGroups);
}
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',
items: [
{ key: 'Uplink', value: `${prettyBytes(traffic.up)}/s` },
{ key: 'Downlink', value: `${prettyBytes(traffic.down)}/s` },
],
});
container!.replaceChildren(renderedWidget);
}
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',
items: [
{ key: 'Uplink', value: String(prettyBytes(connections.uploadTotal)) },
{
key: 'Downlink',
value: String(prettyBytes(connections.downloadTotal)),
},
],
});
container!.replaceChildren(renderedWidget);
}
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',
items: [
{
key: 'Active Connections',
value: String(connections.connections.length),
},
{ key: 'Memory Usage', value: String(prettyBytes(connections.memory)) },
],
});
container!.replaceChildren(renderedWidget);
}
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',
items: [
{
key: 'Podkop',
value: String(services.podkop),
},
{ key: 'Sing-box', value: String(services.singbox) },
],
});
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) {
renderDashboardSections();
}
if (diff?.traffic) {
renderTrafficWidget();
}
if (diff?.connections) {
renderTrafficTotalWidget();
renderSystemInfoWidget();
}
if (diff?.services) {
renderServiceInfoWidget();
}
});
onMount('dashboard-status').then(() => {
console.log('Mounting dashboard');
// Initial sections fetch
fetchDashboardSections();
fetchServicesInfo();
connectToClashSockets();
});
}

View File

@@ -0,0 +1,78 @@
export 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',
}),
]),
],
);
}

View File

@@ -0,0 +1,49 @@
import { Podkop } from '../../podkop/types';
export function renderOutboundGroup({
outbounds,
displayName,
}: Podkop.OutboundGroup) {
function renderOutbound(outbound: Podkop.Outbound) {
return E(
'div',
{
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''}`,
},
[
E('b', {}, outbound.displayName),
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [
E(
'div',
{ class: 'pdk_dashboard-page__outbound-grid__item__type' },
outbound.type,
),
E(
'div',
{ class: 'pdk_dashboard-page__outbound-grid__item__latency' },
outbound.latency ? `${outbound.latency}ms` : 'N/A',
),
]),
],
);
}
return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [
// Title with test latency
E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [
E(
'div',
{
class: 'pdk_dashboard-page__outbound-section__title-section__title',
},
displayName,
),
E('button', { class: 'btn' }, 'Test latency'),
]),
E(
'div',
{ class: 'pdk_dashboard-page__outbound-grid' },
outbounds.map((outbound) => renderOutbound(outbound)),
),
]);
}

View File

@@ -0,0 +1,16 @@
interface IRenderWidgetParams {
title: string;
items: Array<{
key: string;
value: string;
}>;
}
export function renderDashboardWidget({ title, items }: IRenderWidgetParams) {
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
E('b', {}, title),
...items.map((item) =>
E('div', {}, [E('span', {}, `${item.key}: `), E('span', {}, item.value)]),
),
]);
}

View File

@@ -0,0 +1,11 @@
export function getClashApiUrl(): string {
const { protocol, hostname } = window.location;
return `${protocol}//${hostname}:9090`;
}
export function getClashWsUrl(): string {
const { hostname } = window.location;
return `ws://${hostname}:9090`;
}

View File

@@ -0,0 +1,13 @@
export function getProxyUrlName(url: string) {
try {
const [_link, hash] = url.split('#');
if (!hash) {
return '';
}
return decodeURIComponent(hash);
} catch {
return '';
}
}

View File

@@ -5,3 +5,6 @@ export * from './withTimeout';
export * from './executeShellCommand'; export * from './executeShellCommand';
export * from './copyToClipboard'; export * from './copyToClipboard';
export * from './maskIP'; export * from './maskIP';
export * from './getProxyUrlName';
export * from './onMount';
export * from './getClashApiUrl';

View File

@@ -0,0 +1,30 @@
export async function onMount(id: string): Promise<HTMLElement> {
return new Promise((resolve) => {
const el = document.getElementById(id);
if (el && el.offsetParent !== null) {
return resolve(el);
}
const observer = new MutationObserver(() => {
const target = document.getElementById(id);
if (target) {
const io = new IntersectionObserver((entries) => {
const visible = entries.some((e) => e.isIntersecting);
if (visible) {
observer.disconnect();
io.disconnect();
resolve(target);
}
});
io.observe(target);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}

View File

@@ -0,0 +1,12 @@
// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js
export function prettyBytes(n: number) {
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if (n < 1000) {
return n + ' B';
}
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
n = Number((n / Math.pow(1000, exponent)).toPrecision(3));
const unit = UNITS[exponent];
return n + ' ' + unit;
}

View File

@@ -1,3 +1,15 @@
type HtmlTag = keyof HTMLElementTagNameMap;
type HtmlElement<T extends HtmlTag> = HTMLElementTagNameMap[T];
type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
Omit<HtmlElement<T>, 'style' | 'children'> & {
style?: string | Partial<CSSStyleDeclaration>;
class?: string;
onclick?: (event: MouseEvent) => void;
}
>;
declare global { declare global {
const fs: { const fs: {
exec( exec(
@@ -10,6 +22,17 @@ declare global {
code?: number; code?: number;
}>; }>;
}; };
const E: <T extends HtmlTag>(
type: T,
attr?: HtmlAttributes<T> | null,
children?: (Node | string)[] | Node | string,
) => HTMLElementTagNameMap[T];
const uci: {
load: (packages: string | string[]) => Promise<string>;
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
};
} }
export {}; export {};

View File

@@ -1,8 +1,10 @@
'use strict'; 'use strict';
'require baseclass'; 'require baseclass';
'require fs'; 'require fs';
'require uci';
export * from './validators'; export * from './validators';
export * from './helpers'; export * from './helpers';
export * from './clash'; export * from './clash';
export * from './dashboard';
export * from './constants'; export * from './constants';

View File

@@ -0,0 +1,5 @@
import { Podkop } from '../types';
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
return uci.load('podkop').then(() => uci.sections('podkop'));
}

View File

@@ -0,0 +1,115 @@
import { Podkop } from '../types';
import { getConfigSections } from './getConfigSections';
import { getClashProxies } from '../../clash';
import { getProxyUrlName } from '../../helpers';
export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
const configSections = await getConfigSections();
const clashProxies = await getClashProxies();
const clashProxiesData = clashProxies.success
? clashProxies.data
: { proxies: [] };
const proxies = Object.entries(clashProxiesData.proxies).map(
([key, value]) => ({
code: key,
value,
}),
);
return configSections
.filter((section) => section.mode !== 'block')
.map((section) => {
if (section.mode === 'proxy') {
if (section.proxy_config_type === 'url') {
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
return {
code: section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || section['.name'],
displayName:
getProxyUrlName(section.proxy_string) ||
outbound?.value?.name ||
'',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
},
],
};
}
if (section.proxy_config_type === 'outbound') {
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
return {
code: section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || section['.name'],
displayName:
decodeURIComponent(JSON.parse(section.outbound_json)?.tag) ||
outbound?.value?.name ||
'',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
},
],
};
}
if (section.proxy_config_type === 'urltest') {
const selector = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-urltest-out`,
);
const outbounds = (outbound?.value?.all ?? [])
.map((code) => proxies.find((item) => item.code === code))
.map((item, index) => ({
code: item?.code || '',
displayName:
getProxyUrlName(section.urltest_proxy_links?.[index]) ||
item?.value?.name ||
'',
latency: item?.value?.history?.[0]?.delay || 0,
type: item?.value?.type || '',
selected: selector?.value?.now === item?.code,
}));
return {
code: section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || '',
displayName: 'Fastest',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: selector?.value?.now === outbound?.code,
},
...outbounds,
],
};
}
}
return {
code: section['.name'],
displayName: section['.name'],
outbounds: [],
};
});
}

View File

@@ -0,0 +1,21 @@
import { executeShellCommand } from '../../helpers';
export async function getPodkopStatus(): Promise<{
enabled: number;
status: string;
}> {
const response = await executeShellCommand({
command: '/usr/bin/podkop',
args: ['get_status'],
timeout: 1000,
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
enabled: number;
status: string;
};
}
return { enabled: 0, status: 'unknown' };
}

View File

@@ -0,0 +1,23 @@
import { executeShellCommand } from '../../helpers';
export async function getSingboxStatus(): Promise<{
running: number;
enabled: number;
status: string;
}> {
const response = await executeShellCommand({
command: '/usr/bin/podkop',
args: ['get_sing_box_status'],
timeout: 1000,
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
running: number;
enabled: number;
status: string;
};
}
return { running: 0, enabled: 0, status: 'unknown' };
}

View File

@@ -0,0 +1,4 @@
export * from './getConfigSections';
export * from './getDashboardSections';
export * from './getPodkopStatus';
export * from './getSingboxStatus';

View File

@@ -0,0 +1,55 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Podkop {
export interface Outbound {
code: string;
displayName: string;
latency: number;
type: string;
selected: boolean;
}
export interface OutboundGroup {
code: string;
displayName: string;
outbounds: Outbound[];
}
export interface ConfigProxyUrlTestSection {
mode: 'proxy';
proxy_config_type: 'urltest';
urltest_proxy_links: string[];
}
export interface ConfigProxyUrlSection {
mode: 'proxy';
proxy_config_type: 'url';
proxy_string: string;
}
export interface ConfigProxyOutboundSection {
mode: 'proxy';
proxy_config_type: 'outbound';
outbound_json: string;
}
export interface ConfigVpnSection {
mode: 'vpn';
interface: string;
}
export interface ConfigBlockSection {
mode: 'block';
}
export type ConfigBaseSection =
| ConfigProxyUrlTestSection
| ConfigProxyUrlSection
| ConfigProxyOutboundSection
| ConfigVpnSection
| ConfigBlockSection;
export type ConfigSection = ConfigBaseSection & {
'.name': string;
'.type': 'main' | 'extra';
};
}

View File

@@ -0,0 +1,93 @@
// eslint-disable-next-line
type Listener = (data: any) => 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 constructor() {}
static getInstance(): SocketManager {
if (!SocketManager.instance) {
SocketManager.instance = new SocketManager();
}
return SocketManager.instance;
}
connect(url: string): void {
if (this.sockets.has(url)) return;
const ws = new WebSocket(url);
this.sockets.set(url, ws);
this.connected.set(url, false);
this.listeners.set(url, new Set());
ws.addEventListener('open', () => {
this.connected.set(url, true);
console.log(`✅ Connected: ${url}`);
});
ws.addEventListener('message', (event) => {
const handlers = this.listeners.get(url);
if (handlers) {
for (const handler of handlers) {
try {
handler(event.data);
} catch (err) {
console.error(`Handler error for ${url}:`, err);
}
}
}
});
ws.addEventListener('close', () => {
this.connected.set(url, false);
console.warn(`⚠️ Disconnected: ${url}`);
});
ws.addEventListener('error', (err) => {
console.error(`❌ Socket error for ${url}:`, err);
});
}
subscribe(url: string, listener: Listener): void {
if (!this.sockets.has(url)) {
this.connect(url);
}
this.listeners.get(url)?.add(listener);
}
unsubscribe(url: string, listener: Listener): void {
this.listeners.get(url)?.delete(listener);
}
// eslint-disable-next-line
send(url: string, data: any): void {
const ws = this.sockets.get(url);
if (ws && this.connected.get(url)) {
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
} else {
console.warn(`⚠️ Cannot send: not connected to ${url}`);
}
}
disconnect(url: string): void {
const ws = this.sockets.get(url);
if (ws) {
ws.close();
this.sockets.delete(url);
this.listeners.delete(url);
this.connected.delete(url);
}
}
disconnectAll(): void {
for (const url of this.sockets.keys()) {
this.disconnect(url);
}
}
}
export const socket = SocketManager.getInstance();

View File

@@ -0,0 +1,82 @@
import { Podkop } from './podkop/types';
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 listeners = new Set<Listener<T>>();
constructor(initial: T) {
this.value = initial;
}
get(): T {
return this.value;
}
set(next: Partial<T>): void {
const prev = this.value;
const merged = { ...this.value, ...next };
if (Object.is(prev, merged)) return;
this.value = merged;
const diff: Partial<T> = {};
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: Listener<T>): () => void {
this.listeners.add(cb);
cb(this.value, this.value, {}); // первый вызов без diff
return () => this.listeners.delete(cb);
}
patch<K extends keyof T>(key: K, value: T[K]): void {
this.set({ ...this.value, [key]: value });
}
getKey<K extends keyof T>(key: K): T[K] {
return this.value[key];
}
subscribeKey<K extends keyof T>(
key: K,
cb: (value: T[K]) => void,
): () => void {
let prev = this.value[key];
const unsub = this.subscribe((val) => {
if (val[key] !== prev) {
prev = val[key];
cb(val[key]);
}
});
return unsub;
}
}
export const store = new Store<{
sections: Podkop.OutboundGroup[];
traffic: { up: number; down: number };
memory: { inuse: number; oslimit: number };
connections: {
connections: unknown[];
downloadTotal: number;
memory: number;
uploadTotal: number;
};
services: {
singbox: string;
podkop: string;
};
}>({
sections: [],
traffic: { up: 0, down: 0 },
memory: { inuse: 0, oslimit: 0 },
connections: { connections: [], memory: 0, downloadTotal: 0, uploadTotal: 0 },
services: { singbox: '', podkop: '' },
});

View File

@@ -23,4 +23,139 @@ export const GlobalStyles = `
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra { #cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
display: none; display: none;
} }
#cbi-podkop-main-_status > div {
width: 100%;
}
.pdk_dashboard-page {
width: 100%;
--dashboard-grid-columns: 4;
}
@media (max-width: 900px) {
.pdk_dashboard-page {
--dashboard-grid-columns: 2;
}
}
/*@media (max-width: 440px) {*/
/* .pdk_dashboard-page {*/
/* --dashboard-grid-columns: 1;*/
/* }*/
/*}*/
.pdk_dashboard-page__title-section {
display: flex;
align-items: center;
justify-content: space-between;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 0 10px;
}
.pdk_dashboard-page__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__widgets-section {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__widgets-section__item {
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section {
margin-top: 10px;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section__title-section {
display: flex;
align-items: center;
justify-content: space-between;
}
.pdk_dashboard-page__outbound-section__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__outbound-grid {
margin-top: 5px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__outbound-grid__item {
cursor: pointer;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
transition: border 0.2s ease;
}
.pdk_dashboard-page__outbound-grid__item:hover {
border-color: var(--primary-color-high);
}
.pdk_dashboard-page__outbound-grid__item--active {
border-color: var(--success-color-medium);
}
.pdk_dashboard-page__outbound-grid__item__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.pdk_dashboard-page__outbound-grid__item__type {
}
.pdk_dashboard-page__outbound-grid__item__latency {
}
/* Skeleton styles*/
.skeleton {
background-color: var(--background-color-low, #e0e0e0);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 150%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: skeleton-shimmer 1.6s infinite;
}
@keyframes skeleton-shimmer {
100% {
left: 150%;
}
}
`; `;

View File

@@ -0,0 +1,84 @@
import 'dotenv/config';
import chokidar from 'chokidar';
import SFTPClient from 'ssh2-sftp-client';
import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
const sftp = new SFTPClient();
const config = {
host: process.env.SFTP_HOST,
port: Number(process.env.SFTP_PORT || 22),
username: process.env.SFTP_USER,
...(process.env.SFTP_PRIVATE_KEY
? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) }
: { password: process.env.SFTP_PASS }),
};
const localDir = path.resolve(process.env.LOCAL_DIR || './dist');
const remoteDir = process.env.REMOTE_DIR || '/www/luci-static/mypkg';
async function uploadFile(filePath) {
const relativePath = path.relative(localDir, filePath);
const remotePath = path.posix.join(remoteDir, relativePath);
console.log(`⬆️ Uploading: ${relativePath} -> ${remotePath}`);
try {
await sftp.fastPut(filePath, remotePath);
console.log(`✅ Uploaded: ${relativePath}`);
} catch (err) {
console.error(`❌ Failed: ${relativePath}: ${err.message}`);
}
}
async function deleteFile(filePath) {
const relativePath = path.relative(localDir, filePath);
const remotePath = path.posix.join(remoteDir, relativePath);
console.log(`🗑 Removing: ${relativePath}`);
try {
await sftp.delete(remotePath);
console.log(`✅ Removed: ${relativePath}`);
} catch (err) {
console.warn(`⚠️ Could not delete ${relativePath}: ${err.message}`);
}
}
async function uploadAllFiles() {
console.log('🚀 Uploading all files from', localDir);
const files = await glob(`${localDir}/**/*`, { nodir: true });
for (const file of files) {
await uploadFile(file);
}
console.log('✅ Initial upload complete!');
}
async function main() {
await sftp.connect(config);
console.log(`✅ Connected to ${config.host}`);
// 🔹 Загрузить всё при старте
await uploadAllFiles();
// 🔹 Затем следить за изменениями
chokidar
.watch(localDir, { ignoreInitial: true })
.on('all', async (event, filePath) => {
if (event === 'add' || event === 'change') {
await uploadFile(filePath);
} else if (event === 'unlink') {
await deleteFile(filePath);
}
});
process.on('SIGINT', async () => {
console.log('🔌 Disconnecting...');
await sftp.end();
process.exit();
});
}
main().catch(console.error);

View File

@@ -221,6 +221,18 @@
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@isaacs/balanced-match@^4.0.1":
version "4.0.1"
resolved "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
"@isaacs/brace-expansion@^5.0.0":
version "5.0.0"
resolved "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
dependencies:
"@isaacs/balanced-match" "^4.0.1"
"@isaacs/cliui@^8.0.2": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -628,6 +640,13 @@ argparse@^2.0.1:
resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
asn1@^0.2.6:
version "0.2.6"
resolved "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
dependencies:
safer-buffer "~2.1.0"
assertion-error@^2.0.1: assertion-error@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
@@ -638,6 +657,13 @@ balanced-match@^1.0.0:
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bcrypt-pbkdf@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
dependencies:
tweetnacl "^0.14.3"
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.12" version "1.1.12"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
@@ -660,6 +686,16 @@ braces@^3.0.3:
dependencies: dependencies:
fill-range "^7.1.1" fill-range "^7.1.1"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buildcheck@~0.0.6:
version "0.0.6"
resolved "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
bundle-require@^5.1.0: bundle-require@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee"
@@ -701,7 +737,7 @@ check-error@^2.1.1:
resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
chokidar@^4.0.3: chokidar@4.0.3, chokidar@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
@@ -730,6 +766,16 @@ concat-map@0.0.1:
resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
concat-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.0.2"
typedarray "^0.0.6"
confbox@^0.1.8: confbox@^0.1.8:
version "0.1.8" version "0.1.8"
resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
@@ -740,6 +786,14 @@ consola@^3.4.0:
resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==
cpu-features@~0.0.10:
version "0.0.10"
resolved "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5"
integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==
dependencies:
buildcheck "~0.0.6"
nan "^2.19.0"
cross-spawn@^7.0.6: cross-spawn@^7.0.6:
version "7.0.6" version "7.0.6"
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -766,6 +820,11 @@ deep-is@^0.1.3:
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
dotenv@17.2.3:
version "17.2.3"
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2"
integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==
eastasianwidth@^0.2.0: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -1014,7 +1073,7 @@ flatted@^3.2.9:
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
foreground-child@^3.1.0: foreground-child@^3.1.0, foreground-child@^3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
@@ -1041,6 +1100,18 @@ glob-parent@^6.0.2:
dependencies: dependencies:
is-glob "^4.0.3" is-glob "^4.0.3"
glob@11.0.3:
version "11.0.3"
resolved "https://registry.npmmirror.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6"
integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==
dependencies:
foreground-child "^3.3.1"
jackspeak "^4.1.1"
minimatch "^10.0.3"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^2.0.0"
glob@^10.3.10: glob@^10.3.10:
version "10.4.5" version "10.4.5"
resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
@@ -1091,6 +1162,11 @@ imurmurhash@^0.1.4:
resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
inherits@^2.0.3:
version "2.0.4"
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
is-extglob@^2.1.1: is-extglob@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1127,6 +1203,13 @@ jackspeak@^3.1.2:
optionalDependencies: optionalDependencies:
"@pkgjs/parseargs" "^0.11.0" "@pkgjs/parseargs" "^0.11.0"
jackspeak@^4.1.1:
version "4.1.1"
resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
dependencies:
"@isaacs/cliui" "^8.0.2"
joycon@^3.1.1: joycon@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
@@ -1216,6 +1299,11 @@ lru-cache@^10.2.0:
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^11.0.0:
version "11.2.2"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
magic-string@^0.30.17: magic-string@^0.30.17:
version "0.30.19" version "0.30.19"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
@@ -1236,6 +1324,13 @@ micromatch@^4.0.8:
braces "^3.0.3" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
minimatch@^10.0.3:
version "10.0.3"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa"
integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==
dependencies:
"@isaacs/brace-expansion" "^5.0.0"
minimatch@^3.1.2: minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -1279,6 +1374,11 @@ mz@^2.7.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nan@^2.19.0, nan@^2.23.0:
version "2.23.0"
resolved "https://registry.npmmirror.com/nan/-/nan-2.23.0.tgz#24aa4ddffcc37613a2d2935b97683c1ec96093c6"
integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==
nanoid@^3.3.11: nanoid@^3.3.11:
version "3.3.11" version "3.3.11"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@@ -1350,6 +1450,14 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0" lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-scurry@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
dependencies:
lru-cache "^11.0.0"
minipass "^7.1.2"
pathe@^2.0.1, pathe@^2.0.3: pathe@^2.0.1, pathe@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
@@ -1425,6 +1533,15 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
readable-stream@^3.0.2:
version "3.6.2"
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@^4.0.1: readdirp@^4.0.1:
version "4.1.2" version "4.1.2"
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
@@ -1483,6 +1600,16 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^7.6.0: semver@^7.6.0:
version "7.7.2" version "7.7.2"
resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
@@ -1522,6 +1649,25 @@ source-map@0.8.0-beta.0:
dependencies: dependencies:
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
ssh2-sftp-client@12.0.1:
version "12.0.1"
resolved "https://registry.npmmirror.com/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz#926764878954dbed85f6f9233ce7980bfc94fdd4"
integrity sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==
dependencies:
concat-stream "^2.0.0"
ssh2 "^1.16.0"
ssh2@^1.16.0:
version "1.17.0"
resolved "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz#dc686e8e3abdbd4ad95d46fa139615903c12258c"
integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==
dependencies:
asn1 "^0.2.6"
bcrypt-pbkdf "^1.0.2"
optionalDependencies:
cpu-features "~0.0.10"
nan "^2.23.0"
stackback@0.0.2: stackback@0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
@@ -1559,6 +1705,13 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2" emoji-regex "^9.2.2"
strip-ansi "^7.0.1" strip-ansi "^7.0.1"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -1711,6 +1864,11 @@ tsup@8.5.0:
tinyglobby "^0.2.11" tinyglobby "^0.2.11"
tree-kill "^1.2.2" tree-kill "^1.2.2"
tweetnacl@^0.14.3:
version "0.14.5"
resolved "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
type-check@^0.4.0, type-check@~0.4.0: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -1718,6 +1876,11 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies: dependencies:
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-eslint@8.45.0: typescript-eslint@8.45.0:
version "8.45.0" version "8.45.0"
resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b" resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
@@ -1745,6 +1908,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vite-node@3.2.4: vite-node@3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"

View File

@@ -0,0 +1,22 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require uci';
'require fs';
'require view.podkop.utils as utils';
'require view.podkop.main as main';
function createDashboardSection(mainSection) {
let o = mainSection.tab('dashboard', _('Dashboard'));
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => main.renderDashboard();
}
const EntryPoint = {
createDashboardSection,
}
return baseclass.extend(EntryPoint);

View File

@@ -2,6 +2,7 @@
"use strict"; "use strict";
"require baseclass"; "require baseclass";
"require fs"; "require fs";
"require uci";
// src/validators/validateIp.ts // src/validators/validateIp.ts
function validateIPV4(ip) { function validateIPV4(ip) {
@@ -370,6 +371,141 @@ var GlobalStyles = `
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra { #cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
display: none; display: none;
} }
#cbi-podkop-main-_status > div {
width: 100%;
}
.pdk_dashboard-page {
width: 100%;
--dashboard-grid-columns: 4;
}
@media (max-width: 900px) {
.pdk_dashboard-page {
--dashboard-grid-columns: 2;
}
}
/*@media (max-width: 440px) {*/
/* .pdk_dashboard-page {*/
/* --dashboard-grid-columns: 1;*/
/* }*/
/*}*/
.pdk_dashboard-page__title-section {
display: flex;
align-items: center;
justify-content: space-between;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 0 10px;
}
.pdk_dashboard-page__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__widgets-section {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__widgets-section__item {
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section {
margin-top: 10px;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section__title-section {
display: flex;
align-items: center;
justify-content: space-between;
}
.pdk_dashboard-page__outbound-section__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__outbound-grid {
margin-top: 5px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__outbound-grid__item {
cursor: pointer;
border: 2px var(--background-color-low) solid;
border-radius: 4px;
padding: 10px;
transition: border 0.2s ease;
}
.pdk_dashboard-page__outbound-grid__item:hover {
border-color: var(--primary-color-high);
}
.pdk_dashboard-page__outbound-grid__item--active {
border-color: var(--success-color-medium);
}
.pdk_dashboard-page__outbound-grid__item__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.pdk_dashboard-page__outbound-grid__item__type {
}
.pdk_dashboard-page__outbound-grid__item__latency {
}
/* Skeleton styles*/
.skeleton {
background-color: var(--background-color-low, #e0e0e0);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 150%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: skeleton-shimmer 1.6s infinite;
}
@keyframes skeleton-shimmer {
100% {
left: 150%;
}
}
`; `;
// src/helpers/injectGlobalStyles.ts // src/helpers/injectGlobalStyles.ts
@@ -557,6 +693,57 @@ function maskIP(ip = "") {
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`); return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
} }
// src/helpers/getProxyUrlName.ts
function getProxyUrlName(url) {
try {
const [_link, hash] = url.split("#");
if (!hash) {
return "";
}
return decodeURIComponent(hash);
} catch {
return "";
}
}
// src/helpers/onMount.ts
async function onMount(id) {
return new Promise((resolve) => {
const el = document.getElementById(id);
if (el && el.offsetParent !== null) {
return resolve(el);
}
const observer = new MutationObserver(() => {
const target = document.getElementById(id);
if (target) {
const io = new IntersectionObserver((entries) => {
const visible = entries.some((e) => e.isIntersecting);
if (visible) {
observer.disconnect();
io.disconnect();
resolve(target);
}
});
io.observe(target);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
// src/helpers/getClashApiUrl.ts
function getClashApiUrl() {
const { protocol, hostname } = window.location;
return `${protocol}//${hostname}:9090`;
}
function getClashWsUrl() {
const { hostname } = window.location;
return `ws://${hostname}:9090`;
}
// src/clash/methods/createBaseApiRequest.ts // src/clash/methods/createBaseApiRequest.ts
async function createBaseApiRequest(fetchFn) { async function createBaseApiRequest(fetchFn) {
try { try {
@@ -583,7 +770,7 @@ async function createBaseApiRequest(fetchFn) {
// src/clash/methods/getConfig.ts // src/clash/methods/getConfig.ts
async function getClashConfig() { async function getClashConfig() {
return createBaseApiRequest( return createBaseApiRequest(
() => fetch("http://192.168.160.129:9090/configs", { () => fetch(`${getClashApiUrl()}/configs`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}) })
@@ -592,7 +779,7 @@ async function getClashConfig() {
// src/clash/methods/getGroupDelay.ts // src/clash/methods/getGroupDelay.ts
async function getClashGroupDelay(group, url = "https://www.gstatic.com/generate_204", timeout = 2e3) { async function getClashGroupDelay(group, url = "https://www.gstatic.com/generate_204", timeout = 2e3) {
const endpoint = `http://192.168.160.129:9090/group/${group}/delay?url=${encodeURIComponent( const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
url url
)}&timeout=${timeout}`; )}&timeout=${timeout}`;
return createBaseApiRequest( return createBaseApiRequest(
@@ -606,7 +793,7 @@ async function getClashGroupDelay(group, url = "https://www.gstatic.com/generate
// src/clash/methods/getProxies.ts // src/clash/methods/getProxies.ts
async function getClashProxies() { async function getClashProxies() {
return createBaseApiRequest( return createBaseApiRequest(
() => fetch("http://192.168.160.129:9090/proxies", { () => fetch(`${getClashApiUrl()}/proxies`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}) })
@@ -616,12 +803,553 @@ async function getClashProxies() {
// src/clash/methods/getVersion.ts // src/clash/methods/getVersion.ts
async function getClashVersion() { async function getClashVersion() {
return createBaseApiRequest( return createBaseApiRequest(
() => fetch("http://192.168.160.129:9090/version", { () => fetch(`${getClashApiUrl()}/version`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}) })
); );
} }
// src/clash/methods/triggerProxySelector.ts
async function triggerProxySelector(selector, outbound) {
return createBaseApiRequest(
() => fetch(`${getClashApiUrl()}/proxies/${selector}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: outbound })
})
);
}
// 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"));
}
// src/podkop/methods/getDashboardSections.ts
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(
([key, value]) => ({
code: key,
value
})
);
return configSections.filter((section) => section.mode !== "block").map((section) => {
if (section.mode === "proxy") {
if (section.proxy_config_type === "url") {
const outbound = proxies.find(
(proxy) => proxy.code === `${section[".name"]}-out`
);
return {
code: section[".name"],
displayName: section[".name"],
outbounds: [
{
code: outbound?.code || section[".name"],
displayName: getProxyUrlName(section.proxy_string) || outbound?.value?.name || "",
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || "",
selected: true
}
]
};
}
if (section.proxy_config_type === "outbound") {
const outbound = proxies.find(
(proxy) => proxy.code === `${section[".name"]}-out`
);
return {
code: section[".name"],
displayName: section[".name"],
outbounds: [
{
code: outbound?.code || section[".name"],
displayName: decodeURIComponent(JSON.parse(section.outbound_json)?.tag) || outbound?.value?.name || "",
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || "",
selected: true
}
]
};
}
if (section.proxy_config_type === "urltest") {
const selector = proxies.find(
(proxy) => proxy.code === `${section[".name"]}-out`
);
const outbound = proxies.find(
(proxy) => proxy.code === `${section[".name"]}-urltest-out`
);
const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item, index) => ({
code: item?.code || "",
displayName: getProxyUrlName(section.urltest_proxy_links?.[index]) || item?.value?.name || "",
latency: item?.value?.history?.[0]?.delay || 0,
type: item?.value?.type || "",
selected: selector?.value?.now === item?.code
}));
return {
code: section[".name"],
displayName: section[".name"],
outbounds: [
{
code: outbound?.code || "",
displayName: "Fastest",
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || "",
selected: selector?.value?.now === outbound?.code
},
...outbounds
]
};
}
}
return {
code: section[".name"],
displayName: section[".name"],
outbounds: []
};
});
}
// src/podkop/methods/getPodkopStatus.ts
async function getPodkopStatus() {
const response = await executeShellCommand({
command: "/usr/bin/podkop",
args: ["get_status"],
timeout: 1e3
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, ""));
}
return { enabled: 0, status: "unknown" };
}
// src/podkop/methods/getSingboxStatus.ts
async function getSingboxStatus() {
const response = await executeShellCommand({
command: "/usr/bin/podkop",
args: ["get_sing_box_status"],
timeout: 1e3
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, ""));
}
return { running: 0, enabled: 0, status: "unknown" };
}
// src/dashboard/renderer/renderOutboundGroup.ts
function renderOutboundGroup({
outbounds,
displayName
}) {
function renderOutbound(outbound) {
return E(
"div",
{
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""}`
},
[
E("b", {}, outbound.displayName),
E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [
E(
"div",
{ class: "pdk_dashboard-page__outbound-grid__item__type" },
outbound.type
),
E(
"div",
{ class: "pdk_dashboard-page__outbound-grid__item__latency" },
outbound.latency ? `${outbound.latency}ms` : "N/A"
)
])
]
);
}
return E("div", { class: "pdk_dashboard-page__outbound-section" }, [
// Title with test latency
E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [
E(
"div",
{
class: "pdk_dashboard-page__outbound-section__title-section__title"
},
displayName
),
E("button", { class: "btn" }, "Test latency")
]),
E(
"div",
{ class: "pdk_dashboard-page__outbound-grid" },
outbounds.map((outbound) => renderOutbound(outbound))
)
]);
}
// src/store.ts
var Store = class {
constructor(initial) {
this.listeners = /* @__PURE__ */ new Set();
this.value = initial;
}
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: "", podkop: "" }
});
// src/socket.ts
var SocketManager = class _SocketManager {
constructor() {
this.sockets = /* @__PURE__ */ new Map();
this.listeners = /* @__PURE__ */ new Map();
this.connected = /* @__PURE__ */ new Map();
}
static getInstance() {
if (!_SocketManager.instance) {
_SocketManager.instance = new _SocketManager();
}
return _SocketManager.instance;
}
connect(url) {
if (this.sockets.has(url)) return;
const ws = new WebSocket(url);
this.sockets.set(url, ws);
this.connected.set(url, false);
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);
if (handlers) {
for (const handler of handlers) {
try {
handler(event.data);
} catch (err) {
console.error(`Handler error for ${url}:`, err);
}
}
}
});
ws.addEventListener("close", () => {
this.connected.set(url, false);
console.warn(`\u26A0\uFE0F Disconnected: ${url}`);
});
ws.addEventListener("error", (err) => {
console.error(`\u274C Socket error for ${url}:`, err);
});
}
subscribe(url, listener) {
if (!this.sockets.has(url)) {
this.connect(url);
}
this.listeners.get(url)?.add(listener);
}
unsubscribe(url, listener) {
this.listeners.get(url)?.delete(listener);
}
// eslint-disable-next-line
send(url, data) {
const ws = this.sockets.get(url);
if (ws && this.connected.get(url)) {
ws.send(typeof data === "string" ? data : JSON.stringify(data));
} else {
console.warn(`\u26A0\uFE0F Cannot send: not connected to ${url}`);
}
}
disconnect(url) {
const ws = this.sockets.get(url);
if (ws) {
ws.close();
this.sockets.delete(url);
this.listeners.delete(url);
this.connected.delete(url);
}
}
disconnectAll() {
for (const url of this.sockets.keys()) {
this.disconnect(url);
}
}
};
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", {}, title),
...items.map(
(item) => E("div", {}, [E("span", {}, `${item.key}: `), E("span", {}, item.value)])
)
]);
}
// src/helpers/prettyBytes.ts
function prettyBytes(n) {
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
if (n < 1e3) {
return n + " B";
}
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
n = Number((n / Math.pow(1e3, exponent)).toPrecision(3));
const unit = UNITS[exponent];
return n + " " + unit;
}
// src/dashboard/initDashboardController.ts
async function fetchDashboardSections() {
const sections = await getDashboardSections();
store.set({ sections });
}
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 ? "\u2714 Enabled" : singbox.status,
podkop: podkop.status ? "\u2714 Enabled" : podkop.status
}
});
}
async function connectToClashSockets() {
socket.subscribe(`${getClashWsUrl()}/traffic?token=`, (msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
traffic: { up: parsedMsg.up, down: parsedMsg.down }
});
});
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 }
});
});
}
async function renderDashboardSections() {
const sections = store.get().sections;
console.log("render dashboard sections group");
const container = document.getElementById("dashboard-sections-grid");
const renderedOutboundGroups = sections.map(renderOutboundGroup);
container.replaceChildren(...renderedOutboundGroups);
}
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",
items: [
{ key: "Uplink", value: `${prettyBytes(traffic.up)}/s` },
{ key: "Downlink", value: `${prettyBytes(traffic.down)}/s` }
]
});
container.replaceChildren(renderedWidget);
}
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",
items: [
{ key: "Uplink", value: String(prettyBytes(connections.uploadTotal)) },
{
key: "Downlink",
value: String(prettyBytes(connections.downloadTotal))
}
]
});
container.replaceChildren(renderedWidget);
}
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",
items: [
{
key: "Active Connections",
value: String(connections.connections.length)
},
{ key: "Memory Usage", value: String(prettyBytes(connections.memory)) }
]
});
container.replaceChildren(renderedWidget);
}
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",
items: [
{
key: "Podkop",
value: String(services.podkop)
},
{ key: "Sing-box", value: String(services.singbox) }
]
});
container.replaceChildren(renderedWidget);
}
async function initDashboardController() {
store.subscribe((next, prev, diff) => {
console.log("Store changed", { prev, next, diff });
if (diff?.sections) {
renderDashboardSections();
}
if (diff?.traffic) {
renderTrafficWidget();
}
if (diff?.connections) {
renderTrafficTotalWidget();
renderSystemInfoWidget();
}
if (diff?.services) {
renderServiceInfoWidget();
}
});
onMount("dashboard-status").then(() => {
console.log("Mounting dashboard");
fetchDashboardSections();
fetchServicesInfo();
connectToClashSockets();
});
}
return baseclass.extend({ return baseclass.extend({
ALLOWED_WITH_RUSSIA_INSIDE, ALLOWED_WITH_RUSSIA_INSIDE,
BOOTSTRAP_DNS_SERVER_OPTIONS, BOOTSTRAP_DNS_SERVER_OPTIONS,
@@ -645,13 +1373,20 @@ return baseclass.extend({
createBaseApiRequest, createBaseApiRequest,
executeShellCommand, executeShellCommand,
getBaseUrl, getBaseUrl,
getClashApiUrl,
getClashConfig, getClashConfig,
getClashGroupDelay, getClashGroupDelay,
getClashProxies, getClashProxies,
getClashVersion, getClashVersion,
getClashWsUrl,
getProxyUrlName,
initDashboardController,
injectGlobalStyles, injectGlobalStyles,
maskIP, maskIP,
onMount,
parseValueList, parseValueList,
renderDashboard,
triggerProxySelector,
validateDNS, validateDNS,
validateDomain, validateDomain,
validateIPV4, validateIPV4,

View File

@@ -5,6 +5,7 @@
'require view.podkop.configSection as configSection'; 'require view.podkop.configSection as configSection';
'require view.podkop.diagnosticTab as diagnosticTab'; 'require view.podkop.diagnosticTab as diagnosticTab';
'require view.podkop.additionalTab as additionalTab'; 'require view.podkop.additionalTab as additionalTab';
'require view.podkop.dashboardTab as dashboardTab';
'require view.podkop.utils as utils'; 'require view.podkop.utils as utils';
'require view.podkop.main as main'; 'require view.podkop.main as main';
@@ -12,26 +13,31 @@ const EntryNode = {
async render() { async render() {
main.injectGlobalStyles(); main.injectGlobalStyles();
main.getClashVersion() // main.getClashVersion()
.then(result => console.log('getClashVersion - then', result)) // .then(result => console.log('getClashVersion - then', result))
.catch(err => console.log('getClashVersion - err', err)) // .catch(err => console.log('getClashVersion - err', err))
.finally(() => console.log('getClashVersion - finish')); // .finally(() => console.log('getClashVersion - finish'));
//
main.getClashConfig() // main.getClashConfig()
.then(result => console.log('getClashConfig - then', result)) // .then(result => console.log('getClashConfig - then', result))
.catch(err => console.log('getClashConfig - err', err)) // .catch(err => console.log('getClashConfig - err', err))
.finally(() => console.log('getClashConfig - finish')); // .finally(() => console.log('getClashConfig - finish'));
//
main.getClashProxies() // main.getClashProxies()
.then(result => console.log('getClashProxies - then', result)) // .then(result => console.log('getClashProxies - then', result))
.catch(err => console.log('getClashProxies - err', err)) // .catch(err => console.log('getClashProxies - err', err))
.finally(() => console.log('getClashProxies - finish')); // .finally(() => console.log('getClashProxies - finish'));
const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']);
// Main Section // Main Section
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)