some updates

This commit is contained in:
MoonDev
2025-05-22 21:57:13 +03:00
parent 5d47f71e0a
commit a775fe1c80
15 changed files with 425 additions and 110 deletions

Binary file not shown.

44
callbacks/app.py Normal file
View File

@@ -0,0 +1,44 @@
import subprocess
from pathlib import Path
from typing import Dict, Any
async def run_app(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Запускает приложение или ярлык по указанному пути.
Args:
args: Dictionary containing 'url' (string) of the website to open.
Returns:
Dictionary with status message and input arguments.
Raises:
OSError: If opening the website fails.
"""
try:
path = args.get("path")
# Проверка существования файла
if not Path(path).exists():
raise OSError(f"Файл не найден: {path}")
# Проверка расширения файла
if not path.lower().endswith(('.exe', '.lnk', '.bat')):
raise OSError(f"Файл {path} может не быть исполняемым файлом или ярлыком")
# Попытка запуска приложения
subprocess.run(['start', '', path], shell=True, check=True)
return {
"message": f"App {path} opened successfully",
"args": args
}
except FileNotFoundError as e:
raise OSError(f"Файл не найден или путь некорректен: {str(e)}")
except subprocess.CalledProcessError as e:
raise OSError(f"Ошибка при запуске приложения: {str(e)}")
except PermissionError as e:
raise OSError(f"Недостаточно прав для запуска: {str(e)}")
except OSError as e:
raise OSError(f"Ошибка операционной системы при запуске: {str(e)}")
except Exception as e:
raise OSError(f"Непредвиденная ошибка: {str(e)}")

71
config/frontend.yaml Normal file
View File

@@ -0,0 +1,71 @@
- type: "break" # Разделитель с подписью "система"
label: "Система"
- type: "clock" # Часы
mini_icon: "/icons/mini/clock-five.png"
- type: "gauge" # Круговые индикаторы
gauges:
- value: 0
label: "Загрузка CPU"
- value: 0
label: "Занято RAM"
tag: "system_load"
mini_icon: "/icons/mini/chart-line-up.png"
- type: "number" # Вывод числа
value: 0
label: "Трафик Mbit/s"
tag: "net_traffic"
mini_icon: "/icons/mini/wifi.png"
- type: "slider" # Слайдеры
label: "Настройки"
sliders:
- value: 0
label: "Яркость экрана"
action: "screen.chenge_brightness"
- value: 0
label: "Громкость звука"
action: "media.set_volume"
mini_icon: "/icons/mini/settings.png"
- type: "break" # Разделитель
label: "Сайты"
- type: "image" # Кнопка с картинкой
imageUrl: "/icons/telegram-logo-svgrepo-com.svg"
label: "Telegram"
action: "web.open_url"
action_args:
url: "https://ya.ru "
mini_icon: "/icons/mini/arrow-up-right-from-square.png"
- type: "image" # Кнопка с картинкой
imageUrl: "/icons/cloud.png"
label: "Cloud"
action: "web.open_url"
action_args:
url: "https://cloud.viadev.su "
mini_icon: "/icons/mini/arrow-up-right-from-square.png"
- type: "break" # Разделитель
label: "Приложения"
- type: "image" # Кнопка с картинкой
imageUrl: "/icons/steam-svgrepo-com.svg"
label: "Steam"
action: "app.run_app"
action_args:
path: "C:/Users/Slava/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Steam/Steam.lnk"
mini_icon: "/icons/mini/arrow-up-right-from-square.png"
- type: "break" # Разделитель
label: "Управление ПК"
- type: "image" # Кнопка с картинкой
imageUrl: "/icons/vpn.png"
label: "VPN"
action: "v2ray.enable_vpn"
action_args: {}

1
static/frontend/.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_PORT = 8000

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.9.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.7"
@@ -3247,6 +3248,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.9.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.7"

View File

@@ -1,9 +1,9 @@
import axios from 'axios';
const API_URL = "http://192.168.2.151:8000"
const createSha256Hash = async (input) => {
export const createSha256Hash = async (input) => {
try {
// Convert string to array buffer
const msgBuffer = new TextEncoder().encode(input);
@@ -23,11 +23,11 @@ const createSha256Hash = async (input) => {
}
const sendRequest = async (action,args) => {
export const sendRequest = async (action,args) => {
try {
const TS = Math.round(Date.now()/1000);
const hash = await createSha256Hash("<password>"+JSON.stringify(args)+TS);
const response = await axios.post(API_URL+"/action/"+action, {args:args,hash:hash+"."+TS}, {
const hash = await createSha256Hash(window.localStorage.auth+JSON.stringify(args)+TS);
const response = await axios.post("http://"+window.location.hostname+":"+import.meta.env.VITE_API_PORT+"/action/"+action, {args:args,hash:hash+"."+TS}, {
timeout: 4000,
headers: {
'Content-Type': 'application/json'
@@ -40,4 +40,3 @@ const sendRequest = async (action,args) => {
}
};
export default sendRequest;

View File

@@ -7,12 +7,17 @@
transition: all 0.3s ease;
}
.glass:hover {
background: rgba(147, 51, 234, 0.2);
border-color: rgba(147, 51, 234, 0.7);
box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3);
.glass {
&:hover,
&:focus,
&:active {
background: rgba(147, 51, 234, 0.2);
border-color: rgba(147, 51, 234, 0.7);
box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3);
}
}
@keyframes flipRight {
0% {
transform: rotate(0deg);

View File

@@ -1,6 +1,8 @@
import { useState, useEffect } from 'react'
import './App.css'
import sendRequest from "./Api.jsx";
import { sendRequest, createSha256Hash } from "./Api.jsx";
import { QRCodeSVG } from 'qrcode.react';
import axios from 'axios';
window.sendRequest = sendRequest;
const TextTile = ({ label }) => (
@@ -94,25 +96,16 @@ const NumberTile = ({ value, label }) => (
);
const App = () => {
const [tiles, setTiles] = useState([
{ id: 0, type: "break", label: "Система" },
{ id: 1, type: "clock", mini_icon:"/icons/mini/clock-five.png" },
const [phase, setPhase] = useState("loading");
const [QrLink, setQrLink] = useState();
const [Password, setPassword] = useState("");
{ id: 2, type: "gauge", gauges: [{ value: 3.60 * 0, label: "Загрузка CPU" }, { value: 3.60 * 0, label: "Занято RAM" }], tag: "system_load", mini_icon:"/icons/mini/chart-line-up.png" },
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
{ id: 3, type: "number", value: 0, label: "Трафик Mbit/s", tag: "net_traffic", mini_icon:"/icons/mini/wifi.png" },
{ id: 4, type: "slider", label: "Настройки", sliders: [{ value: 50, label: "Яроксть экрана", action: "screen.chenge_brightness" }, { value: 75, label: "Громкость звука", action: "media.set_volume" }], mini_icon:"/icons/mini/settings.png" },
{ id: 5, type: "break", label: "Сайты" },
{ id: 6, type: "image", imageUrl: "/icons/steam-svgrepo-com.svg", label: "Steam", action: "web.open_url", action_args: { url: "https://ya.ru" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" },
{ id: 7, type: "image", imageUrl: "/icons/telegram-logo-svgrepo-com.svg", label: "Telegram", action: "web.open_url", action_args: { url: "https://ya.ru" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" },
{ id: 8, type: "image", imageUrl: "/icons/cloud.png", label: "Cloud", action: "web.open_url", action_args: { url: "https://cloud.viadev.su" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" },
{ id: 9, type: "break", label: "Управление ПК" },
{ id: 10, type: "image", imageUrl: "/icons/vpn.png", label: "VPN", action: "v2ray.enable_vpn", action_args: {} },
]);
const [tiles, setTiles] = useState();
@@ -133,17 +126,6 @@ const App = () => {
};
const toggleImageEnabled = (tileId) => {
setTiles(prevTiles =>
prevTiles.map(tile => {
if (tile.id === tileId && tile.type === "image") {
return { ...tile, enabled: !tile.enabled };
}
return tile;
})
);
};
const setTileProps = (tileId, props) => {
setTiles(prevTiles =>
prevTiles.map(tile => {
@@ -164,29 +146,88 @@ const App = () => {
})
);
};
const updateSystemStats = async () => {
try {
const data = await sendRequest("system.get_system_metrics", {});
setTilePropsByTag("system_load", {
gauges: [
{ value: (3.60 * data.result.metrics.cpu_usage_percent).toFixed(0), label: "Загрузка CPU" },
{ value: (3.60 * (data.result.metrics.ram_used_gb / data.result.metrics.ram_total_gb) * 100).toFixed(0), label: "Занято RAM" }
]
})
setTilePropsByTag("net_traffic", {
value: data.result.metrics.network_traffic_mbps
})
} catch (e) {
console.error(e);
}
};
useEffect(() => {
const updateSystemStats = async () => {
try {
const data = await sendRequest("system.get_system_metrics", {});
setTilePropsByTag("system_load", {
gauges: [
{ value: (3.60 * data.result.metrics.cpu_usage_percent).toFixed(0), label: "Загрузка CPU" },
{ value: (3.60 * (data.result.metrics.ram_used_gb / data.result.metrics.ram_total_gb) * 100).toFixed(0), label: "Занято RAM" }
]
})
setTilePropsByTag("net_traffic", {
value: data.result.metrics.network_traffic_mbps
})
const TryAuth = async (passw) => {
try {
setPhase("loading");
const hash = await createSha256Hash("frontend_config" + passw);
const response = await axios.get("http://" + window.location.hostname + ":" + import.meta.env.VITE_API_PORT + "/frontend-config", {
timeout: 4000,
params: {
hash: hash,
},
headers: {
'Content-Type': 'application/json'
}
});
setTimeout(() => {
window.localStorage.auth = passw;
setTiles(response.data);
setPhase("dash");
updateSystemStats();
const interval = setInterval(updateSystemStats, 2000);
}, 500)
} catch (e) {
console.error(e);
} catch (e) {
window.localStorage.clear("auth")
window.location.reload();
}
}
const RequestAuth = () => {
setPhase("login");
}
const Init = async () => {
if (window.localStorage.auth) {
return TryAuth(window.localStorage.auth);
}
const response = await axios.get("http://" + window.location.hostname + ":" + import.meta.env.VITE_API_PORT + "/primary-ip", {
timeout: 4000,
headers: {
'Content-Type': 'application/json'
}
};
updateSystemStats();
const interval = setInterval(updateSystemStats, 2000);
return () => clearInterval(interval);
});
console.log(response.data)
const currentUrl = new URL(window.location.href);
// Меняем хост
currentUrl.hostname = response.data;
// Устанавливаем hash
currentUrl.hash = '#skip_start';
// Получаем новый URL
const newUrl = currentUrl.toString();
setQrLink(newUrl);
if (window.location.hash != "#skip_start") {
setPhase("start");
} else {
setPhase("login");
}
}
useEffect(() => {
Init();
// updateSystemStats();
// const interval = setInterval(updateSystemStats, 2000);
// return () => { clearInterval(interval) };
}, []);
@@ -214,53 +255,111 @@ const App = () => {
}
}
return (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 lg:grid-cols-6 flex-wrap gap-4">
{tiles.map(tile => (
<div key={tile.id} className={tile.type === "break" ? "col-span-full" : (tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" || tile.type === "number" ? "w-full sm:w-auto col-span-2" : "col-span-1")}>
{tile.type === "break" ? (
<div className="w-full mt-2 text-purple-400 text-2xl font-bold">{tile.label}</div>
) : (
<div
onClick={() => { onTileClick(tile) }}
className={`glass rounded-lg ${tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" ? "w-full h-45 lg:h-48" : "w-full h-45 lg:h-48"} relative ${tile.type === "image" && tile.enabled ? "glass_enabled" : ""}`}
>
{tile.loading &&
<div className={"flex animate-[fadeIn_0.5s_ease-out] absolute inset-0 bg-black/10 backdrop-blur-[3px] flex-col items-center justify-center z-10 rounded-xl"}>
<div className="w-10 h-10 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" style={{ animationDuration: "0.5s" }} ></div>
</div>
}
{!tile.loading &&
<>
{tile.type === "text" && <TextTile label={tile.label} />}
{tile.type === "image" && (
<ImageTile
imageUrl={tile.imageUrl}
label={tile.label}
enabled={tile.enabled}
toggleEnabled={() => toggleImageEnabled(tile.id)}
/>
)}
{tile.type === "clock" && <ClockTile id={tile.id} />}
{tile.type === "gauge" && <GaugeTile gauges={tile.gauges} />}
{tile.type === "slider" && <SliderTile label={tile.label} sliders={tile.sliders} tileId={tile.id} updateSliderValue={updateSliderValue} />}
{tile.type === "number" && <NumberTile value={tile.value} label={tile.label} />}
{tile.mini_icon && (
<div className="absolute top-3 right-3 flex gap-2">
<img src={tile.mini_icon} className='h-5 w-5'/>
</div>
)}
</>
}
if (phase === "start") {
return (
<div className='h-screen w-full flex justify-center items-center anim-pop'>
<div className='flex gap-3 w-2xl text-gray-200 divide-violet-500 divide-x-2 items-center animate-[fadeIn_0.5s_ease-out]'>
<div className='w-full px-2 hidden sm:block'>
<div className='flex flex-col gap-4 items-center justify-center'>
<div className='p-4 rounded-lg bg-white'>
<QRCodeSVG value={QrLink} size={200} />
</div>
)}
</div>
</div>
))}
<div className='w-full px-2'>
<div className='h-full flex items-center justify-center'>
<button
onClick={RequestAuth}
className="glass cursor-pointer rounded-lg px-4 py-2 text-purple-400 font-semibold text-xl transition-all duration-300"
>
Продолжить в браузере
</button>
</div>
</div>
</div>
</div>
</div>
)
}
if (phase === "loading") {
return (
<div className={"flex animate-[fadeIn_0.5s_ease-out] absolute inset-0 bg-black/10 backdrop-blur-[3px] flex-col items-center justify-center z-10 rounded-xl anim-pop"}>
<div className="w-10 h-10 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" style={{ animationDuration: "0.5s" }} ></div>
</div>
)
}
return (
<>
{phase == "login" && (
<div className='h-screen w-full flex justify-center items-center anim-pop'>
<div className='flex gap-3 w-xl'>
<form onSubmit={() => { TryAuth(Password) }} className='w-full'>
<input
type="password"
name="password"
value={Password}
onChange={handlePasswordChange}
placeholder="Введите пароль панели"
className="glass w-full rounded-lg px-4 py-2 text-purple-400 font-semibold text-lg placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-purple-500 hover:text-purple-200 transition-all duration-300"
/>
</form>
</div>
</div>
)}
{phase === "dash" && (
<div className="flex w-full flex-col gap-4 anim-pop">
<div className="grid grid-cols-2 lg:grid-cols-6 flex-wrap gap-4">
{tiles.map(tile => (
<div key={tile.id} className={tile.type === "break" ? "col-span-full" : (tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" || tile.type === "number" ? "w-full sm:w-auto col-span-2" : "col-span-1")}>
{tile.type === "break" ? (
<div className="w-full mt-2 text-purple-400 text-2xl font-bold">{tile.label}</div>
) : (
<div
onClick={() => { onTileClick(tile) }}
className={`glass rounded-lg ${tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" ? "w-full h-45 lg:h-48" : "w-full h-45 lg:h-48"} relative ${tile.type === "image" && tile.enabled ? "glass_enabled" : ""}`}
>
{tile.loading &&
<div className={"flex animate-[fadeIn_0.5s_ease-out] absolute inset-0 bg-black/10 backdrop-blur-[3px] flex-col items-center justify-center z-10 rounded-xl"}>
<div className="w-10 h-10 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" style={{ animationDuration: "0.5s" }} ></div>
</div>
}
{!tile.loading &&
<>
{tile.type === "text" && <TextTile label={tile.label} />}
{tile.type === "image" && (
<ImageTile
imageUrl={tile.imageUrl}
label={tile.label}
enabled={tile.enabled}
/>
)}
{tile.type === "clock" && <ClockTile id={tile.id} />}
{tile.type === "gauge" && <GaugeTile gauges={tile.gauges} />}
{tile.type === "slider" && <SliderTile label={tile.label} sliders={tile.sliders} tileId={tile.id} updateSliderValue={updateSliderValue} />}
{tile.type === "number" && <NumberTile value={tile.value} label={tile.label} />}
{tile.mini_icon && (
<div className="absolute top-3 right-3 flex gap-2">
<img src={tile.mini_icon} className='h-5 w-5' />
</div>
)}
</>
}
</div>
)}
</div>
))}
</div>
</div>
)};
</>
);
};
export default App

View File

@@ -1,4 +1,23 @@
@import "tailwindcss";
#root{
html,body,#root{
width:100%;
height: 100%;
background-color: #101828;
}
@keyframes anim-pop {
0% {
opacity: 0;
transform: scale(.95);
transform: scale(var(--btn-focus-scale, .98));
}
100% {
opacity: 1;
transform: scale(1);
}
}
.anim-pop {
opacity: 0; /* Element starts hidden */
animation: anim-pop .3s ease-in forwards;
animation-delay: 0.2s;
}

View File

@@ -4,7 +4,7 @@ import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<body className='bg-gray-900 min-h-screen py-4 flex justify-center px-3 2xl:px-[15%]'>
<div className='bg-gray-900 min-h-screen pt-4 pb-8 flex justify-center px-3 2xl:px-[15%]'>
<App />
</body>
</div>
)

Binary file not shown.

23
utils/get_primary_ip.py Normal file
View File

@@ -0,0 +1,23 @@
import netifaces
def get_primary_ip():
try:
# Получаем список всех интерфейсов
interfaces = netifaces.interfaces()
for iface in interfaces:
# Пропускаем loopback интерфейс
if iface == 'lo':
continue
# Получаем данные об интерфейсе
iface_data = netifaces.ifaddresses(iface)
# Проверяем наличие IPv4 адреса
if netifaces.AF_INET in iface_data:
for addr in iface_data[netifaces.AF_INET]:
ip = addr['addr']
# Исключаем локальные адреса
if not ip.startswith('127.'):
return ip
return "IP не найден"
except Exception as e:
return f"Ошибка: {e}"

View File

@@ -10,8 +10,11 @@ from fastapi.responses import JSONResponse
import hashlib
import json
import time
import yaml
from pathlib import Path
from utils.get_primary_ip import get_primary_ip
PASSWORD = "<password>"
PASSWORD = "10010055"
@@ -29,11 +32,6 @@ app.add_middleware(
# Static files & UI server
app.mount("/static", StaticFiles(directory="static"), name="static")
# Pydantic model for request payload
class CommandModel(BaseModel):
args: Dict[str, Any]
hash: str # Mandatory hash field
# Show main page
@app.get("/")
async def read_index():
@@ -41,6 +39,51 @@ async def read_index():
html_content = f.read()
return HTMLResponse(content=html_content, status_code=200)
# Pydantic model for request payload
class CommandModel(BaseModel):
args: Dict[str, Any]
hash: str # Mandatory hash field
@app.get("/primary-ip", response_model=None)
def get_config():
return get_primary_ip()
@app.get("/frontend-config", response_model=None)
def get_config(hash: str):
computed_hash = hashlib.sha256( ("frontend_config"+PASSWORD).encode("utf-8")).hexdigest()
# Verify hash
if computed_hash != hash:
raise HTTPException(status_code=401, detail="Invalid hash")
CONFIG_FILE = Path("config/frontend.yaml")
# Checking the existence of the file
if not CONFIG_FILE.exists():
raise HTTPException(status_code=404, detail="Файл frontend.yaml не найден")
try:
# YAML reading and parsing
with open(CONFIG_FILE, "r", encoding="utf-8") as file:
data = yaml.safe_load(file)
# Adding an id
for idx, item in enumerate(data):
item["id"] = idx
return data
except yaml.YAMLError as e:
# YAML parsing error
raise HTTPException(status_code=500, detail=f"Ошибка в формате YAML: {e}")
except Exception as e:
# Any other error
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {e}")
# Process actions via POST
@app.post("/action/{name:path}")
async def handle_action(name: str, payload: CommandModel):