some updates
This commit is contained in:
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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user