some updates
This commit is contained in:
Binary file not shown.
BIN
callbacks/__pycache__/app.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
44
callbacks/app.py
Normal file
44
callbacks/app.py
Normal 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
71
config/frontend.yaml
Normal 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
1
static/frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_PORT = 8000
|
||||
10
static/frontend/package-lock.json
generated
10
static/frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
BIN
utils/__pycache__/get_primary_ip.cpython-311.pyc
Normal file
BIN
utils/__pycache__/get_primary_ip.cpython-311.pyc
Normal file
Binary file not shown.
23
utils/get_primary_ip.py
Normal file
23
utils/get_primary_ip.py
Normal 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}"
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user