Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,040 changes: 289 additions & 751 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "gml-frontend",
"version": "25.3.2",
"version": "25.3.3",
"private": true,
"scripts": {
"dev": "next dev --experimental-https",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand Down Expand Up @@ -45,7 +45,7 @@
"js-cookie": "3.0.5",
"lodash": "4.17.21",
"lucide-react": "0.407.0",
"next": "^14.2.32",
"next": "^14.2.35",
"next-themes": "0.3.0",
"react": "18",
"react-day-picker": "8.10.1",
Expand All @@ -64,7 +64,7 @@
},
"devDependencies": {
"@feature-sliced/eslint-config": "0.1.1",
"@playwright/test": "1.45.1",
"@playwright/test": "^1.57.0",
"@types/js-cookie": "3.0.6",
"@types/lodash": "4.17.13",
"@types/node": "20",
Expand All @@ -76,7 +76,7 @@
"autoprefixer": "10.4.19",
"eslint": "8.57.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-next": "14.2.6",
"eslint-config-next": "14.2.35",
"eslint-config-prettier": "9.1.0",
"eslint-config-react-app": "7.0.1",
"eslint-config-standard-with-typescript": "43.0.1",
Expand All @@ -93,5 +93,8 @@
"tailwindcss": "3.4.17",
"typescript": "5.5.3",
"zustand": "4.5.4"
},
"overrides": {
"glob": "10.5.0"
}
}
34 changes: 34 additions & 0 deletions src/app/auth/signin/AutoRefreshOnAuthPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";

import { authService } from "@/shared/services";
import { DASHBOARD_PAGES } from "@/shared/routes";

/**
* On the auth page, if a refresh token (HttpOnly cookie) exists and the access token is
* missing/expired, try to refresh immediately so the user is auto-signed in.
* If refresh fails, we silently keep the user on the sign-in page.
*/
export function AutoRefreshOnAuthPage() {
const router = useRouter();
const tried = useRef(false);

useEffect(() => {
if (tried.current) return;
tried.current = true;

// Fire-and-forget; errors are ignored to avoid disrupting manual sign-in
authService
.refresh()
.then(() => {
router.replace(DASHBOARD_PAGES.HOME);
})
.catch(() => {
// stay on sign-in
});
}, [router]);

return null;
}
3 changes: 3 additions & 0 deletions src/app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import Image from 'next/image';

import classes from './styles.module.css';
import { AutoRefreshOnAuthPage } from './AutoRefreshOnAuthPage';

import { LoginPluginScriptViewer, SignInForm } from '@/features/auth-credentials-form';
import logo from '@/assets/logos/logo.svg';

export default function Page() {
return (
<>
{/* Try to automatically refresh using HttpOnly refresh token and redirect to dashboard */}
<AutoRefreshOnAuthPage />
<div className={classes.login}>
<div className={classes['login__main-content']}>
<div className={classes.login__form}>
Expand Down
2 changes: 1 addition & 1 deletion src/app/mnt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function MntPage() {
</CardContent>
<CardFooter className="justify-end">
<div className=" action-block flex gap-2">
<Button id="restoreButton" variant="secondary">
<Button id="restoreButton" variant="secondary" onClick={() => router.push('/mnt/restore')}>
Восстановить из резервной копии
</Button>
<Button id="proceedButton" onClick={() => router.push('/mnt/license')}>
Expand Down
178 changes: 178 additions & 0 deletions src/app/mnt/restore/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
'use client';

import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

import { config } from '@/core/configs';
import { settingsService } from '@/shared/services/SettingsService';
import { isAxiosError } from '@/shared/lib/isAxiosError/isAxiosError';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/shared/ui/card';
import { Button } from '@/shared/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select';
import { Progress } from '@/shared/ui/progress';
import logo from '@/assets/logos/logo.svg';

export default function MntRestorePage() {
const router = useRouter();
const [keys, setKeys] = useState<string[]>([]);
const [loadingKeys, setLoadingKeys] = useState<boolean>(false);
const [selectedKey, setSelectedKey] = useState<string>('');
const [restoring, setRestoring] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0);
const progressTimerRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
let ignore = false;
async function fetchKeys() {
setLoadingKeys(true);
try {
const { data } = await settingsService.getRestoreKeys();
const list: string[] = Array.isArray(data?.data) ? data.data : [];
if (!ignore) {
setKeys(list);
setSelectedKey((prev) => prev || list[0] || '');
}
} catch (error: any) {
isAxiosError({
toast,
error,
customDescription: 'Не удалось получить список резервных копий',
});
} finally {
if (!ignore) setLoadingKeys(false);
}
}
fetchKeys();
return () => {
ignore = true;
};
}, []);

useEffect(() => {
if (!restoring) {
if (progressTimerRef.current) {
clearInterval(progressTimerRef.current as any);
progressTimerRef.current = null;
}
setProgress(0);
return;
}
// Artificial slow progress that caps at 95% until the request finishes
setProgress(1);
progressTimerRef.current = setInterval(() => {
setProgress((p) => {
const next = p + Math.max(0.4, (100 - p) * 0.015);
return Math.min(next, 95);
});
}, 300);
return () => {
if (progressTimerRef.current) clearInterval(progressTimerRef.current as any);
progressTimerRef.current = null;
};
}, [restoring]);

async function onRestore() {
if (!selectedKey) return;
try {
setRestoring(true);
await settingsService.restoreByKey(selectedKey);
// Finish progress and show success
setProgress(100);
toast.success('Процесс восстановления запущен, завершите установку');
// Redirect to setup page after success
setTimeout(() => router.push('/mnt/setup'), 500);
} catch (error: any) {
isAxiosError({ toast, error, customDescription: 'Не удалось запустить восстановление' });
} finally {
// keep the progress at its current value for a short moment for better UX
setTimeout(() => {
setRestoring(false);
setProgress(0);
}, 700);
}
}

const hasBackups = keys.length > 0;

return (
<div className="min-h-screen p-4 sm:p-10 flex flex-col items-center justify-center gap-6">
<Image src={logo} alt="Gml Frontend" className="w-12 sm:w-16" />
<h1 className="text-3xl font-bold tracking-tight">{config.name} Восстановление</h1>

<div className="grid gap-6 w-full max-w-2xl">
<Card>
<CardHeader>
<CardTitle>Восстановление из резервной копии</CardTitle>
<CardDescription>
Выберите резервную копию и запустите восстановление системы.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{loadingKeys ? (
<div className="text-sm text-muted-foreground">Загрузка списка бекапов…</div>
) : hasBackups ? (
<>
<label className="grid gap-2 text-sm">
<span>Выберите бекап</span>
<Select value={selectedKey} onValueChange={setSelectedKey} disabled={restoring}>
<SelectTrigger>
<SelectValue placeholder="Выберите резервную копию" />
</SelectTrigger>
<SelectContent>
{keys.map((k) => (
<SelectItem key={k} value={k}>
{k}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
{restoring && (
<div className="grid gap-2">
<div className="flex items-center justify-between text-xs">
<span>Восстановление…</span>
<span>{Math.round(progress)}%</span>
</div>
<Progress value={progress} />
</div>
)}
</>
) : (
<div className="text-sm text-muted-foreground">
Что нет ни единого бекапа в системе. Загрузите бекап .gbak в папку
<br />
<span className="font-mono bg-white/10 p-1 rounded-md">/srv/gml/data/backups</span>
</div>
)}
</CardContent>
<CardFooter className="justify-between">
<Button variant="secondary" onClick={() => router.push('/mnt')} disabled={restoring}>
Назад
</Button>
<div className="flex gap-2">
<Button
id="restoreStartButton"
onClick={onRestore}
disabled={!hasBackups || !selectedKey || restoring}
>
{restoring ? 'Восстановление…' : 'Восстановить'}
</Button>
</div>
</CardFooter>
</Card>
<div className=" text-center text-xs text-muted-foreground">
{config.name} {config.version}
</div>
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions src/app/wait/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
'use client';

import { useEffect } from 'react';
import Image from 'next/image';

import logo from '@/assets/logos/logo.svg';
Expand All @@ -13,6 +16,15 @@ import {
import { Icons } from '@/shared/ui/icons';

export default function WaitPage() {
useEffect(() => {
// Reload the page every second to re-check backend availability
const id = setInterval(() => {
// Use location.reload to trigger Next server-side revalidation/route checks
window.location.reload();
}, 2500);
return () => clearInterval(id);
}, []);

return (
<div className="min-h-screen p-4 sm:p-10 flex flex-col items-center justify-center gap-6">
<Image src={logo} alt="Gml Frontend" className="w-12 sm:w-16" />
Expand Down
4 changes: 4 additions & 0 deletions src/features/edit-settings-platform-form/lib/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export const EditSettingsPlatformSchema = z.object({
storageLogin: z.string(),
storagePassword: z.string(),
textureProtocol: z.number(),
// Sentry auto-clear fields
sentryNeedAutoClear: z.boolean(),
// TimeSpan as string (e.g., "00:05:00" or "1.00:00:00")
sentryAutoClearPeriod: z.string(),
});

export type EditSettingsPlatformSchemaType = z.infer<typeof EditSettingsPlatformSchema>;
Loading