Merge branch 'tests'

This commit is contained in:
Kirill
2026-05-24 19:11:25 +05:00
16 changed files with 1905 additions and 5706 deletions
@@ -0,0 +1,40 @@
import { apiClient } from '@/shared/api/client'
export type ChecklistResultDto = {
passed: boolean
comment: string | null
checkedAt: string
}
export type TestChecklistResponse = {
results: Record<string, ChecklistResultDto>
}
export type UpdateChecklistItemResponse = {
itemKey: string
passed: boolean
comment: string | null
checkedAt: string
}
export async function fetchTestChecklistResults(): Promise<TestChecklistResponse> {
const { data } = await apiClient.get<TestChecklistResponse>('admin/test-checklist')
return data
}
export async function updateTestChecklistItem(
itemKey: string,
passed: boolean,
comment?: string | null,
): Promise<UpdateChecklistItemResponse> {
const { data } = await apiClient.patch<{ result: UpdateChecklistItemResponse }>('admin/test-checklist', {
itemKey,
passed,
comment: passed ? null : comment ?? null,
})
return data.result
}
export async function resetTestChecklist(): Promise<void> {
await apiClient.post('admin/test-checklist/reset')
}
@@ -15,7 +15,17 @@ import Typography from '@mui/material/Typography'
import useMediaQuery from '@mui/material/useMediaQuery'
import { useQuery } from '@tanstack/react-query'
import { useUnit } from 'effector-react'
import { Bell, Image, LayoutGrid, ListOrdered, MessageSquare, Settings, Store, Users } from 'lucide-react'
import {
Bell,
ClipboardCheck,
Image,
LayoutGrid,
ListOrdered,
MessageSquare,
Settings,
Store,
Users,
} from 'lucide-react'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api'
import { AdminCategoriesPage } from '@/pages/admin-categories'
@@ -24,6 +34,7 @@ import { AdminOrdersPage } from '@/pages/admin-orders'
import { AdminProductsPage } from '@/pages/admin-products'
import { AdminReviewsPage } from '@/pages/admin-reviews'
import { AdminSettingsPage } from '@/pages/admin-settings'
import { AdminTestChecklistPage } from '@/pages/admin-test-checklist'
import { AdminUsersPage } from '@/pages/admin-users'
import { $user } from '@/shared/model/auth'
import { ScrollOnNavigate } from '@/shared/ui/ScrollOnNavigate'
@@ -63,6 +74,7 @@ export function AdminLayoutPage() {
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
{ to: '/admin/notifications', label: 'Уведомления', icon: <Bell /> },
{ to: '/admin/settings', label: 'Настройки', icon: <Settings /> },
{ to: '/admin/test-checklist', label: 'Тест-чеклист', icon: <ClipboardCheck /> },
],
[],
)
@@ -193,6 +205,7 @@ export function AdminLayoutPage() {
<Route path="users" element={<AdminUsersPage />} />
<Route path="notifications" element={<AdminNotificationsPage />} />
<Route path="settings" element={<AdminSettingsPage />} />
<Route path="test-checklist" element={<AdminTestChecklistPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Routes>
</Box>
@@ -0,0 +1 @@
export { AdminTestChecklistPage } from './ui/AdminTestChecklistPage'
@@ -0,0 +1,280 @@
import { useCallback, useMemo, useState } from 'react'
import Alert from '@mui/material/Alert'
import Accordion from '@mui/material/Accordion'
import AccordionDetails from '@mui/material/AccordionDetails'
import AccordionSummary from '@mui/material/AccordionSummary'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import CircularProgress from '@mui/material/CircularProgress'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
import IconButton from '@mui/material/IconButton'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import TextField from '@mui/material/TextField'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
import {
fetchTestChecklistResults,
resetTestChecklist,
updateTestChecklistItem,
} from '@/entities/test-checklist/api/test-checklist-api'
type Status = 'passed' | 'failed' | 'unchecked'
function statusFromResult(r: { passed: boolean; comment: string | null } | undefined): Status {
if (!r) return 'unchecked'
return r.passed ? 'passed' : 'failed'
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function StatusIcon({ status, onClick }: { status: Status; onClick: () => void }) {
const icons = {
passed: <CheckCircleIcon sx={{ color: 'success.main' }} />,
failed: <CancelIcon sx={{ color: 'error.main' }} />,
unchecked: <Box sx={{ width: 24, height: 24, borderRadius: '50%', border: 2, borderColor: 'action.disabled' }} />,
}
return (
<Tooltip title={status === 'passed' ? 'Пройдено' : status === 'failed' ? 'Не пройдено' : 'Не проверено'}>
<IconButton size="small" onClick={onClick} sx={{ p: 0.5 }}>
{icons[status]}
</IconButton>
</Tooltip>
)
}
export function AdminTestChecklistPage() {
const queryClient = useQueryClient()
const [confirmOpen, setConfirmOpen] = useState(false)
const [expanded, setExpanded] = useState<string | false>(false)
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
const [errorItemKey, setErrorItemKey] = useState<string | null>(null)
const [errorComment, setErrorComment] = useState('')
const [errorCommentError, setErrorCommentError] = useState(false)
const { data, isLoading, isError } = useQuery({
queryKey: ['admin', 'test-checklist'],
queryFn: fetchTestChecklistResults,
})
const updateMutation = useMutation({
mutationFn: ({ itemKey, passed, comment }: { itemKey: string; passed: boolean; comment?: string | null }) =>
updateTestChecklistItem(itemKey, passed, comment),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
},
})
const resetMutation = useMutation({
mutationFn: resetTestChecklist,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
setConfirmOpen(false)
},
})
const sections = useMemo(() => {
const map = new Map<string, (typeof TEST_CHECKLIST_ITEMS)[number][]>()
for (const item of TEST_CHECKLIST_ITEMS) {
const existing = map.get(item.section) || []
existing.push(item)
map.set(item.section, existing)
}
return Array.from(map.entries())
}, [])
const results = data?.results ?? {}
const total = TEST_CHECKLIST_ITEMS.length
const passedCount = TEST_CHECKLIST_ITEMS.filter((i) => results[i.key]?.passed).length
const handleStatusClick = useCallback(
(itemKey: string) => {
const current = statusFromResult(results[itemKey])
if (current === 'failed') {
updateMutation.mutate({ itemKey, passed: false, comment: null })
} else if (current === 'unchecked') {
updateMutation.mutate({ itemKey, passed: true })
} else {
setErrorItemKey(itemKey)
setErrorComment('')
setErrorCommentError(false)
setErrorDialogOpen(true)
}
},
[results, updateMutation],
)
const handleSaveError = () => {
if (!errorItemKey) return
if (!errorComment.trim()) {
setErrorCommentError(true)
return
}
updateMutation.mutate({ itemKey: errorItemKey, passed: false, comment: errorComment.trim() })
setErrorDialogOpen(false)
setErrorItemKey(null)
setErrorComment('')
}
const errorItem = errorItemKey ? TEST_CHECKLIST_ITEMS.find((i) => i.key === errorItemKey) : null
return (
<Box>
<Stack direction="row" sx={{ mb: 3, justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
Тест-чеклист
</Typography>
<Typography variant="body2" color="text.secondary">
Пройдено: {passedCount} из {total}
</Typography>
</Box>
<Button
variant="outlined"
color="warning"
onClick={() => setConfirmOpen(true)}
disabled={resetMutation.isPending}
>
Сбросить все
</Button>
</Stack>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : isError ? (
<Alert severity="error">Не удалось загрузить чеклист.</Alert>
) : (
sections.map(([section, items]) => (
<Accordion
key={section}
expanded={expanded === section}
onChange={(_, isExpanded) => setExpanded(isExpanded ? section : false)}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography sx={{ fontWeight: 600 }}>{section}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 0 }}>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={{ width: 56 }} />
<TableCell sx={{ fontWeight: 600 }}>Действие</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Ожидаемый результат</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Комментарий</TableCell>
<TableCell sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>Дата проверки</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => {
const r = results[item.key]
const status = statusFromResult(r)
return (
<TableRow key={item.key} hover>
<TableCell>
<StatusIcon status={status} onClick={() => handleStatusClick(item.key)} />
</TableCell>
<TableCell>{item.action}</TableCell>
<TableCell>{item.expectedResult}</TableCell>
<TableCell sx={{ maxWidth: 200 }}>
{r?.comment ? (
<Tooltip title={r.comment} arrow>
<Typography
variant="body2"
color="error.main"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer',
}}
>
{r.comment}
</Typography>
</Tooltip>
) : (
<Typography variant="body2" color="text.disabled">
</Typography>
)}
</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap', color: r ? 'text.primary' : 'text.disabled' }}>
{r ? formatDate(r.checkedAt) : '—'}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
<DialogTitle>Сбросить все проверки?</DialogTitle>
<DialogContent>
<Typography>Все отметки будут удалены. Это действие нельзя отменить.</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Отмена</Button>
<Button color="warning" onClick={() => resetMutation.mutate()}>
Сбросить
</Button>
</DialogActions>
</Dialog>
<Dialog open={errorDialogOpen} onClose={() => setErrorDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Описание ошибки {errorItem?.action}</DialogTitle>
<DialogContent>
<TextField
fullWidth
multiline
rows={4}
value={errorComment}
onChange={(e) => {
setErrorComment(e.target.value)
if (errorCommentError) setErrorCommentError(false)
}}
placeholder="Опишите, что пошло не так..."
error={errorCommentError}
helperText={errorCommentError ? 'Обязательное поле' : ''}
sx={{ mt: 1 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setErrorDialogOpen(false)}>Отмена</Button>
<Button color="error" onClick={handleSaveError}>
Сохранить
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
@@ -0,0 +1,539 @@
# Admin Test Checklist Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an admin page with a manual testing checklist where each check can be marked as passed/failed with auto-timestamp.
**Architecture:** Hardcoded checklist items in shared constants, Prisma model for results, Fastify API route (admin-only), React admin page with MUI Accordion grouped by section.
**Tech Stack:** Prisma (SQLite), Fastify, React + MUI, @tanstack/react-query, apiClient (axios)
---
### Task 1: Prisma Migration — ChecklistResult Model
**Files:**
- Modify: `server/prisma/schema.prisma` (add model at end)
- [ ] **Step 1: Add ChecklistResult model to schema**
Append to `server/prisma/schema.prisma` after the last model:
```prisma
/// Результат ручной проверки тест-чеклиста
model ChecklistResult {
id String @id @default(cuid())
itemKey String @unique
passed Boolean
checkedAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
- [ ] **Step 2: Run migration**
```bash
cd /mnt/d/my_projects/shop/server && npx prisma migrate dev --name add_checklist_result
```
Expected: Migration created and applied successfully.
- [ ] **Step 3: Commit**
```bash
git add server/prisma/schema.prisma server/prisma/migrations/
git commit -m "feat: add ChecklistResult model for manual test checklist"
```
---
### Task 2: Shared Constants — Checklist Items
**Files:**
- Create: `shared/constants/test-checklist-items.js`
- Create: `shared/constants/test-checklist-items.d.ts`
- [ ] **Step 1: Create the JS file with all checklist items**
Create `shared/constants/test-checklist-items.js`:
```js
export const TEST_CHECKLIST_ITEMS = Object.freeze([
// Авторизация
{ key: 'auth.register-email', section: 'Авторизация', action: 'Зарегистрироваться по email', expectedResult: 'Код приходит на почту, аккаунт создаётся' },
{ key: 'auth.login-password', section: 'Авторизация', action: 'Войти по паролю', expectedResult: 'Корректный пароль пускает, неправильный — ошибка' },
{ key: 'auth.oauth-vk', section: 'Авторизация', action: 'Войти через OAuth VK', expectedResult: 'Редирект на VK, callback, авторизация успешна' },
{ key: 'auth.oauth-yandex', section: 'Авторизация', action: 'Войти через OAuth Yandex', expectedResult: 'Редирект на Yandex, callback, авторизация успешна' },
{ key: 'auth.reset-password', section: 'Авторизация', action: 'Сбросить пароль', expectedResult: 'Письмо приходит, ссылка работает, пароль меняется' },
{ key: 'auth.logout', section: 'Авторизация', action: 'Выйти из аккаунта', expectedResult: 'Сессия очищается, редирект на страницу входа' },
// Каталог и товары
{ key: 'catalog.homepage', section: 'Каталог и товары', action: 'Открыть главную страницу', expectedResult: 'Слайдер грузится, товары отображаются' },
{ key: 'catalog.filters', section: 'Каталог и товары', action: 'Применить фильтры', expectedResult: 'Фильтры по категории, цене, материалам работают' },
{ key: 'catalog.product-page', section: 'Каталог и товары', action: 'Открыть страницу товара', expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются' },
{ key: 'catalog.seo', section: 'Каталог и товары', action: 'Проверить SEO-метаданные', expectedResult: 'Title, meta, slug корректные' },
// Корзина
{ key: 'cart.add', section: 'Корзина', action: 'Добавить товар в корзину', expectedResult: 'Счётчик корзины обновляется' },
{ key: 'cart.change-qty', section: 'Корзина', action: 'Изменить количество товара', expectedResult: 'Сумма пересчитывается' },
{ key: 'cart.remove', section: 'Корзина', action: 'Удалить товар из корзины', expectedResult: 'Товар убирается, сумма пересчитывается' },
// Чекаут
{ key: 'checkout.address', section: 'Чекаут', action: 'Выбрать адрес доставки', expectedResult: 'Можно выбрать из сохранённых или добавить новый' },
{ key: 'checkout.delivery', section: 'Чекаут', action: 'Выбрать способ доставки', expectedResult: 'Почта, OZON, Яндекс, 5post — доступны' },
{ key: 'checkout.payment', section: 'Чекаут', action: 'Выбрать способ оплаты', expectedResult: 'Онлайн / при получении — доступны' },
{ key: 'checkout.comment', section: 'Чекаут', action: 'Добавить комментарий к заказу', expectedResult: 'Поле работает, текст сохраняется' },
{ key: 'checkout.create', section: 'Чекаут', action: 'Создать заказ', expectedResult: 'Заказ создаётся, статус DRAFT' },
// Оплата
{ key: 'payment.yookassa', section: 'Оплата', action: 'Оплатить через ЮKassa', expectedResult: 'Редирект на оплату, webhook обрабатывается' },
{ key: 'payment.status', section: 'Оплата', action: 'Проверить статус платежа', expectedResult: 'Статус обновляется после webhook' },
// Профиль пользователя
{ key: 'profile.avatar', section: 'Профиль пользователя', action: 'Управление аватаром', expectedResult: 'Загрузка, отображение, удаление работают' },
{ key: 'profile.settings', section: 'Профиль пользователя', action: 'Изменить настройки профиля', expectedResult: 'Email, имя, способы входа обновляются' },
{ key: 'profile.addresses', section: 'Профиль пользователя', action: 'Управление адресами', expectedResult: 'Добавление, редактирование, удаление, по умолчанию' },
{ key: 'profile.orders', section: 'Профиль пользователя', action: 'Просмотр заказов', expectedResult: 'Список, детали, статусы отображаются' },
{ key: 'profile.messages', section: 'Профиль пользователя', action: 'Сообщения по заказу', expectedResult: 'Отправка, получение, read state работают' },
{ key: 'profile.notifications', section: 'Профиль пользователя', action: 'Настройки уведомлений', expectedResult: 'Вкл/выкл каналов работают' },
{ key: 'profile.delete-account', section: 'Профиль пользователя', action: 'Удалить аккаунт', expectedResult: 'Данные удаляются' },
// Админ — Товары
{ key: 'admin-products.list', section: 'Админ — Товары', action: 'Открыть список товаров', expectedResult: 'Пагинация, поиск работают' },
{ key: 'admin-products.create', section: 'Админ — Товары', action: 'Создать товар', expectedResult: 'Все поля сохраняются, фото загружаются, публикация работает' },
{ key: 'admin-products.edit', section: 'Админ — Товары', action: 'Редактировать товар', expectedResult: 'Изменения сохраняются' },
{ key: 'admin-products.delete', section: 'Админ — Товары', action: 'Удалить товар', expectedResult: 'Товар удаляется' },
{ key: 'admin-products.images', section: 'Админ — Товары', action: 'Управление изображениями товара', expectedResult: 'Добавление, сортировка, удаление работают' },
// Админ — Категории
{ key: 'admin-categories.crud', section: 'Админ — Категории', action: 'CRUD категорий', expectedResult: 'Создание, редактирование, удаление, сортировка работают' },
// Админ — Заказы
{ key: 'admin-orders.list', section: 'Админ — Заказы', action: 'Открыть список заказов', expectedResult: 'Фильтрация по статусу, внимание отображается' },
{ key: 'admin-orders.details', section: 'Админ — Заказы', action: 'Открыть детали заказа', expectedResult: 'Состав, статус, смена статуса работают' },
{ key: 'admin-orders.messages', section: 'Админ — Заказы', action: 'Ответить на сообщение заказа', expectedResult: 'Сообщение отправляется пользователю' },
// Админ — Отзывы
{ key: 'admin-reviews.list', section: 'Админ — Отзывы', action: 'Открыть список отзывов', expectedResult: 'Фильтрация pending/approved/rejected работает' },
{ key: 'admin-reviews.moderate', section: 'Админ — Отзывы', action: 'Модерировать отзыв', expectedResult: 'Approve/reject работают' },
// Админ — Пользователи
{ key: 'admin-users.list', section: 'Админ — Пользователи', action: 'Открыть список пользователей', expectedResult: 'Email, дата регистрации отображаются' },
{ key: 'admin-users.orders', section: 'Админ — Пользователи', action: 'Просмотр заказов пользователя', expectedResult: 'Заказы пользователя отображаются' },
// Админ — Галерея
{ key: 'admin-gallery.upload', section: 'Админ — Галерея', action: 'Управление галереей', expectedResult: 'Загрузка, удаление, использование в слайдере работают' },
// Админ — Настройки
{ key: 'admin-settings.notifications', section: 'Админ — Настройки', action: 'Настройки уведомлений админа', expectedResult: 'Email, telegram настраиваются' },
// Инфо-страницы
{ key: 'info.pages', section: 'Инфо-страницы', action: 'Открыть инфо-страницы', expectedResult: 'Доставка, оплата, как заказать, статусы заказов отображаются' },
{ key: 'info.legal', section: 'Инфо-страницы', action: 'Открыть юридические страницы', expectedResult: 'Политика конфиденциальности, условия использования отображаются' },
// SSE / Realtime
{ key: 'sse.notifications', section: 'SSE / Realtime', action: 'Проверить SSE-уведомления', expectedResult: 'Уведомления приходят в реальном времени' },
])
```
- [ ] **Step 2: Create the TypeScript declaration file**
Create `shared/constants/test-checklist-items.d.ts`:
```ts
export interface TestChecklistItem {
key: string
section: string
action: string
expectedResult: string
}
export declare const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[]
```
- [ ] **Step 3: Commit**
```bash
git add shared/constants/test-checklist-items.js shared/constants/test-checklist-items.d.ts
git commit -m "feat: add test checklist items shared constants"
```
---
### Task 3: Server API Route
**Files:**
- Create: `server/src/routes/api/admin/test-checklist.js`
- Modify: `server/src/routes/api.js` (register new route)
- [ ] **Step 1: Create the API route file**
Create `server/src/routes/api/admin/test-checklist.js`:
```js
import { prisma } from '../../../lib/prisma.js'
export async function registerAdminTestChecklistRoutes(fastify) {
fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async () => {
const results = await prisma.checklistResult.findMany()
const resultMap = {}
for (const r of results) {
resultMap[r.itemKey] = { passed: r.passed, checkedAt: r.checkedAt.toISOString() }
}
return { results: resultMap }
})
fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request) => {
const { itemKey, passed } = request.body || {}
if (!itemKey || typeof passed !== 'boolean') {
return fastify.httpErrors.badRequest('itemKey and passed (boolean) are required')
}
const result = await prisma.checklistResult.upsert({
where: { itemKey },
create: { itemKey, passed },
update: { passed, checkedAt: new Date() },
})
return { result: { itemKey: result.itemKey, passed: result.passed, checkedAt: result.checkedAt.toISOString() } }
})
fastify.post('/api/admin/test-checklist/reset', { preHandler: [fastify.verifyAdmin] }, async () => {
await prisma.checklistResult.deleteMany({})
return { ok: true }
})
}
```
- [ ] **Step 2: Register the route in api.js**
Read `server/src/routes/api.js` to find where other admin routes are registered (look for imports and calls like `registerAdminNotificationRoutes`).
Add import and registration following the existing pattern. Example:
```js
import { registerAdminTestChecklistRoutes } from './api/admin/test-checklist.js'
// ... inside the registerApiRoutes function:
await registerAdminTestChecklistRoutes(fastify)
```
- [ ] **Step 3: Commit**
```bash
git add server/src/routes/api/admin/test-checklist.js server/src/routes/api.js
git commit -m "feat: add admin test-checklist API routes"
```
---
### Task 4: Client — API Layer
**Files:**
- Create: `client/src/entities/test-checklist/api/test-checklist-api.ts`
- [ ] **Step 1: Create the API functions**
Create `client/src/entities/test-checklist/api/test-checklist-api.ts`:
```ts
import { apiClient } from '@/shared/api/client'
export interface ChecklistResultDto {
passed: boolean
checkedAt: string
}
export interface TestChecklistResponse {
results: Record<string, ChecklistResultDto>
}
export async function fetchTestChecklistResults(): Promise<TestChecklistResponse> {
const { data } = await apiClient.get<TestChecklistResponse>('admin/test-checklist')
return data
}
export async function updateTestChecklistItem(itemKey: string, passed: boolean): Promise<{ itemKey: string; passed: boolean; checkedAt: string }> {
const { data } = await apiClient.patch<{ result: { itemKey: string; passed: boolean; checkedAt: string } }>('admin/test-checklist', { itemKey, passed })
return data.result
}
export async function resetTestChecklist(): Promise<void> {
await apiClient.post('admin/test-checklist/reset')
}
```
- [ ] **Step 2: Commit**
```bash
git add client/src/entities/test-checklist/api/test-checklist-api.ts
git commit -m "feat: add test-checklist API client functions"
```
---
### Task 5: Client — Admin Test Checklist Page
**Files:**
- Create: `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx`
- Create: `client/src/pages/admin-test-checklist/index.ts`
- [ ] **Step 1: Create the page component**
Create `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx`:
```tsx
import { useMemo, useState } from 'react'
import Accordion from '@mui/material/Accordion'
import AccordionDetails from '@mui/material/AccordionDetails'
import AccordionSummary from '@mui/material/AccordionSummary'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Checkbox from '@mui/material/Checkbox'
import CircularProgress from '@mui/material/CircularProgress'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Typography from '@mui/material/Typography'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
import { fetchTestChecklistResults, resetTestChecklist, updateTestChecklistItem } from '@/entities/test-checklist/api/test-checklist-api'
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
export function AdminTestChecklistPage() {
const queryClient = useQueryClient()
const [confirmOpen, setConfirmOpen] = useState(false)
const [expanded, setExpanded] = useState<string | false>(false)
const { data, isLoading } = useQuery({
queryKey: ['admin', 'test-checklist'],
queryFn: fetchTestChecklistResults,
})
const updateMutation = useMutation({
mutationFn: ({ itemKey, passed }: { itemKey: string; passed: boolean }) => updateTestChecklistItem(itemKey, passed),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
},
})
const resetMutation = useMutation({
mutationFn: resetTestChecklist,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
setConfirmOpen(false)
},
})
const sections = useMemo(() => {
const map = new Map<string, typeof TEST_CHECKLIST_ITEMS>()
for (const item of TEST_CHECKLIST_ITEMS) {
const existing = map.get(item.section) || []
existing.push(item)
map.set(item.section, existing)
}
return Array.from(map.entries())
}, [])
const results = data?.results ?? {}
const total = TEST_CHECKLIST_ITEMS.length
const passedCount = TEST_CHECKLIST_ITEMS.filter((i) => results[i.key]?.passed).length
return (
<Box>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
Тест-чеклист
</Typography>
<Typography variant="body2" color="text.secondary">
Пройдено: {passedCount} из {total}
</Typography>
</Box>
<Button variant="outlined" color="warning" onClick={() => setConfirmOpen(true)} disabled={resetMutation.isPending}>
Сбросить все
</Button>
</Stack>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : (
sections.map(([section, items]) => (
<Accordion key={section} expanded={expanded === section} onChange={(_, isExpanded) => setExpanded(isExpanded ? section : false)}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography sx={{ fontWeight: 600 }}>{section}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 0 }}>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell padding="checkbox" sx={{ width: 56 }} />
<TableCell sx={{ fontWeight: 600 }}>Действие</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Ожидаемый результат</TableCell>
<TableCell sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>Дата проверки</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => {
const r = results[item.key]
return (
<TableRow key={item.key} hover>
<TableCell padding="checkbox">
<Checkbox
checked={r?.passed ?? false}
onChange={(_, checked) => updateMutation.mutate({ itemKey: item.key, passed: checked })}
color="success"
/>
</TableCell>
<TableCell>{item.action}</TableCell>
<TableCell>{item.expectedResult}</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap', color: r ? 'text.primary' : 'text.disabled' }}>
{r ? formatDate(r.checkedAt) : '—'}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
<DialogTitle>Сбросить все проверки?</DialogTitle>
<DialogContent>
<DialogContentText>Все отметки будут удалены. Это действие нельзя отменить.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Отмена</Button>
<Button color="warning" onClick={() => resetMutation.mutate()} autoFocus>
Сбросить
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
```
- [ ] **Step 2: Create the barrel export**
Create `client/src/pages/admin-test-checklist/index.ts`:
```ts
export { AdminTestChecklistPage } from './ui/AdminTestChecklistPage'
```
- [ ] **Step 3: Commit**
```bash
git add client/src/pages/admin-test-checklist/
git commit -m "feat: add admin test checklist page"
```
---
### Task 6: Client — Routing & Navigation
**Files:**
- Modify: `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx` (add nav item + route)
- [ ] **Step 1: Add nav item and route to AdminLayoutPage**
In `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx`:
1. Add import for `ClipboardCheck` from `lucide-react`:
```tsx
import { Bell, ClipboardCheck, Image, LayoutGrid, ListOrdered, MessageSquare, Settings, Store, Users } from 'lucide-react'
```
2. Add import for the page:
```tsx
import { AdminTestChecklistPage } from '@/pages/admin-test-checklist'
```
3. Add nav item to `navItems` array (after настройки):
```tsx
{ to: '/admin/test-checklist', label: 'Тест-чеклист', icon: <ClipboardCheck /> },
```
4. Add route to `<Routes>` (before the catch-all `*`):
```tsx
<Route path="test-checklist" element={<AdminTestChecklistPage />} />
```
- [ ] **Step 2: Commit**
```bash
git add client/src/pages/admin-layout/ui/AdminLayoutPage.tsx
git commit -m "feat: add test-checklist to admin navigation and routing"
```
---
### Task 7: Verification
- [ ] **Step 1: Run server tests**
```bash
cd /mnt/d/my_projects/shop/server && npm test
```
Expected: All tests pass.
- [ ] **Step 2: Run client lint**
```bash
cd /mnt/d/my_projects/shop/client && npm run lint
```
Expected: No errors.
- [ ] **Step 3: Run client format check**
```bash
cd /mnt/d/my_projects/shop/client && npm run format:check
```
Expected: All files formatted correctly.
- [ ] **Step 4: Run client tests**
```bash
cd /mnt/d/my_projects/shop/client && npm test
```
Expected: All tests pass.
- [ ] **Step 5: Run client build**
```bash
cd /mnt/d/my_projects/shop/client && npm run build
```
Expected: Build succeeds with no type errors.
@@ -0,0 +1,507 @@
# Test Checklist v2 — 3-State Status + Error Comments Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add "failed with error" state to checklist items with a mandatory error comment dialog.
**Architecture:** Add `comment String?` field to `ChecklistResult` model. Update PATCH API to accept optional comment. Replace checkbox with 3-state status selector (passed/failed/not-checked). Show error comment dialog when marking as failed.
**Tech Stack:** Prisma (SQLite), Fastify, React + MUI, @tanstack/react-query, apiClient (axios)
---
### Task 1: Prisma Migration — Add comment field
**Files:**
- Modify: `server/prisma/schema.prisma`
- [ ] **Step 1: Add comment field to ChecklistResult model**
In `server/prisma/schema.prisma`, find the `ChecklistResult` model and add `comment String?` after `passed`:
```prisma
/// Результат ручной проверки тест-чеклиста
model ChecklistResult {
id String @id @default(cuid())
itemKey String @unique
passed Boolean
comment String?
checkedAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
- [ ] **Step 2: Run migration**
```bash
cd /mnt/d/my_projects/shop/server && npx prisma migrate dev --name add_checklist_comment
```
Expected: Migration created and applied successfully.
- [ ] **Step 3: Commit**
```bash
git add server/prisma/schema.prisma server/prisma/migrations/
git commit -m "feat: add comment field to ChecklistResult for error descriptions"
```
---
### Task 2: Server API — Support comment in PATCH and GET
**Files:**
- Modify: `server/src/routes/api/admin/test-checklist.js`
- [ ] **Step 1: Update PATCH to accept and store comment**
Replace the PATCH handler in `server/src/routes/api/admin/test-checklist.js`:
```js
fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
const { itemKey, passed, comment } = request.body || {}
if (!itemKey || typeof passed !== 'boolean') {
return reply.code(400).send({ error: 'itemKey и passed (boolean) обязательны' })
}
if (comment !== undefined && typeof comment !== 'string') {
return reply.code(400).send({ error: 'comment должен быть строкой' })
}
if (comment !== undefined && comment.length > 2000) {
return reply.code(400).send({ error: 'Комментарий слишком длинный (макс. 2000 символов)' })
}
const result = await prisma.checklistResult.upsert({
where: { itemKey },
create: { itemKey, passed, comment: passed ? null : comment || null },
update: { passed, comment: passed ? null : comment ?? undefined, checkedAt: new Date() },
})
return { result: { itemKey: result.itemKey, passed: result.passed, comment: result.comment, checkedAt: result.checkedAt.toISOString() } }
})
```
- [ ] **Step 2: Update GET to include comment in response**
Replace the GET handler's result mapping:
```js
fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
const results = await prisma.checklistResult.findMany()
const resultMap = {}
for (const r of results) {
resultMap[r.itemKey] = { passed: r.passed, comment: r.comment, checkedAt: r.checkedAt.toISOString() }
}
return { results: resultMap }
})
```
- [ ] **Step 3: Commit**
```bash
git add server/src/routes/api/admin/test-checklist.js
git commit -m "feat: support comment field in test-checklist API"
```
---
### Task 3: Client API Layer — Add comment to types and functions
**Files:**
- Modify: `client/src/entities/test-checklist/api/test-checklist-api.ts`
- [ ] **Step 1: Update types and update function**
Replace the entire file content:
```ts
import { apiClient } from '@/shared/api/client'
export type ChecklistResultDto = {
passed: boolean
comment: string | null
checkedAt: string
}
export type TestChecklistResponse = {
results: Record<string, ChecklistResultDto>
}
export type UpdateChecklistItemResponse = {
itemKey: string
passed: boolean
comment: string | null
checkedAt: string
}
export async function fetchTestChecklistResults(): Promise<TestChecklistResponse> {
const { data } = await apiClient.get<TestChecklistResponse>('admin/test-checklist')
return data
}
export async function updateTestChecklistItem(
itemKey: string,
passed: boolean,
comment?: string | null,
): Promise<UpdateChecklistItemResponse> {
const { data } = await apiClient.patch<{ result: UpdateChecklistItemResponse }>('admin/test-checklist', {
itemKey,
passed,
comment: passed ? null : comment ?? null,
})
return data.result
}
export async function resetTestChecklist(): Promise<void> {
await apiClient.post('admin/test-checklist/reset')
}
```
- [ ] **Step 2: Commit**
```bash
git add client/src/entities/test-checklist/api/test-checklist-api.ts
git commit -m "feat: add comment to test-checklist API client types and function"
```
---
### Task 4: Client Page — 3-state status + error comment dialog
**Files:**
- Modify: `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx`
- [ ] **Step 1: Replace the entire page component**
Replace the entire file content:
```tsx
import { useCallback, useMemo, useState } from 'react'
import Alert from '@mui/material/Alert'
import Accordion from '@mui/material/Accordion'
import AccordionDetails from '@mui/material/AccordionDetails'
import AccordionSummary from '@mui/material/AccordionSummary'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import CircularProgress from '@mui/material/CircularProgress'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
import IconButton from '@mui/material/IconButton'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import TextField from '@mui/material/TextField'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
import {
fetchTestChecklistResults,
resetTestChecklist,
updateTestChecklistItem,
} from '@/entities/test-checklist/api/test-checklist-api'
type Status = 'passed' | 'failed' | 'unchecked'
function statusFromResult(r: { passed: boolean; comment: string | null } | undefined): Status {
if (!r) return 'unchecked'
return r.passed ? 'passed' : 'failed'
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function StatusIcon({ status, onClick }: { status: Status; onClick: () => void }) {
const icons = {
passed: <CheckCircleIcon sx={{ color: 'success.main' }} />,
failed: <CancelIcon sx={{ color: 'error.main' }} />,
unchecked: <Box sx={{ width: 24, height: 24, borderRadius: '50%', border: 2, borderColor: 'action.disabled' }} />,
}
return (
<Tooltip title={status === 'passed' ? 'Пройдено' : status === 'failed' ? 'Не пройдено' : 'Не проверено'}>
<IconButton size="small" onClick={onClick} sx={{ p: 0.5 }}>
{icons[status]}
</IconButton>
</Tooltip>
)
}
export function AdminTestChecklistPage() {
const queryClient = useQueryClient()
const [confirmOpen, setConfirmOpen] = useState(false)
const [expanded, setExpanded] = useState<string | false>(false)
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
const [errorItemKey, setErrorItemKey] = useState<string | null>(null)
const [errorComment, setErrorComment] = useState('')
const [errorCommentError, setErrorCommentError] = useState(false)
const { data, isLoading, isError } = useQuery({
queryKey: ['admin', 'test-checklist'],
queryFn: fetchTestChecklistResults,
})
const updateMutation = useMutation({
mutationFn: ({ itemKey, passed, comment }: { itemKey: string; passed: boolean; comment?: string | null }) =>
updateTestChecklistItem(itemKey, passed, comment),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
},
})
const resetMutation = useMutation({
mutationFn: resetTestChecklist,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
setConfirmOpen(false)
},
})
const sections = useMemo(() => {
const map = new Map<string, (typeof TEST_CHECKLIST_ITEMS)[number][]>()
for (const item of TEST_CHECKLIST_ITEMS) {
const existing = map.get(item.section) || []
existing.push(item)
map.set(item.section, existing)
}
return Array.from(map.entries())
}, [])
const results = data?.results ?? {}
const total = TEST_CHECKLIST_ITEMS.length
const passedCount = TEST_CHECKLIST_ITEMS.filter((i) => results[i.key]?.passed).length
const handleStatusClick = useCallback(
(itemKey: string) => {
const current = statusFromResult(results[itemKey])
if (current === 'failed') {
// Clicking on failed → reset to unchecked
updateMutation.mutate({ itemKey, passed: false, comment: null })
} else if (current === 'unchecked') {
// Clicking on unchecked → mark as passed
updateMutation.mutate({ itemKey, passed: true })
} else {
// Clicking on passed → open error dialog
setErrorItemKey(itemKey)
setErrorComment('')
setErrorCommentError(false)
setErrorDialogOpen(true)
}
},
[results, updateMutation],
)
const handleSaveError = () => {
if (!errorItemKey) return
if (!errorComment.trim()) {
setErrorCommentError(true)
return
}
updateMutation.mutate({ itemKey: errorItemKey, passed: false, comment: errorComment.trim() })
setErrorDialogOpen(false)
setErrorItemKey(null)
setErrorComment('')
}
const errorItem = errorItemKey ? TEST_CHECKLIST_ITEMS.find((i) => i.key === errorItemKey) : null
return (
<Box>
<Stack direction="row" sx={{ mb: 3, justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }}>
Тест-чеклист
</Typography>
<Typography variant="body2" color="text.secondary">
Пройдено: {passedCount} из {total}
</Typography>
</Box>
<Button
variant="outlined"
color="warning"
onClick={() => setConfirmOpen(true)}
disabled={resetMutation.isPending}
>
Сбросить все
</Button>
</Stack>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : isError ? (
<Alert severity="error">Не удалось загрузить чеклист.</Alert>
) : (
sections.map(([section, items]) => (
<Accordion
key={section}
expanded={expanded === section}
onChange={(_, isExpanded) => setExpanded(isExpanded ? section : false)}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography sx={{ fontWeight: 600 }}>{section}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 0 }}>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={{ width: 56 }} />
<TableCell sx={{ fontWeight: 600 }}>Действие</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Ожидаемый результат</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Комментарий</TableCell>
<TableCell sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>Дата проверки</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => {
const r = results[item.key]
const status = statusFromResult(r)
return (
<TableRow key={item.key} hover>
<TableCell>
<StatusIcon status={status} onClick={() => handleStatusClick(item.key)} />
</TableCell>
<TableCell>{item.action}</TableCell>
<TableCell>{item.expectedResult}</TableCell>
<TableCell sx={{ maxWidth: 200 }}>
{r?.comment ? (
<Tooltip title={r.comment} arrow>
<Typography
variant="body2"
color="error.main"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer',
}}
>
{r.comment}
</Typography>
</Tooltip>
) : (
<Typography variant="body2" color="text.disabled">
</Typography>
)}
</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap', color: r ? 'text.primary' : 'text.disabled' }}>
{r ? formatDate(r.checkedAt) : '—'}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
<DialogTitle>Сбросить все проверки?</DialogTitle>
<DialogContent>
<Typography>Все отметки будут удалены. Это действие нельзя отменить.</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Отмена</Button>
<Button color="warning" onClick={() => resetMutation.mutate()}>
Сбросить
</Button>
</DialogActions>
</Dialog>
<Dialog open={errorDialogOpen} onClose={() => setErrorDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>
Описание ошибки {errorItem?.action}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
fullWidth
multiline
rows={4}
value={errorComment}
onChange={(e) => {
setErrorComment(e.target.value)
if (errorCommentError) setErrorCommentError(false)
}}
placeholder="Опишите, что пошло не так..."
error={errorCommentError}
helperText={errorCommentError ? 'Обязательное поле' : ''}
sx={{ mt: 1 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setErrorDialogOpen(false)}>Отмена</Button>
<Button color="error" onClick={handleSaveError}>
Сохранить
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
```
- [ ] **Step 2: Commit**
```bash
git add client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx
git commit -m "feat: add 3-state status and error comment dialog to test checklist"
```
---
### Task 5: Verification
- [ ] **Step 1: Run server tests**
```bash
cd /mnt/d/my_projects/shop/server && npm test
```
Expected: All tests pass (pre-existing failures unrelated).
- [ ] **Step 2: Run client lint**
```bash
cd /mnt/d/my_projects/shop/client && npm run lint
```
Expected: 0 errors.
- [ ] **Step 3: Run client format check**
```bash
cd /mnt/d/my_projects/shop/client && npm run format:check
```
Expected: All files formatted correctly.
- [ ] **Step 4: Run client build**
```bash
cd /mnt/d/my_projects/shop/client && npm run build
```
Expected: Build succeeds with no type errors.
@@ -0,0 +1,149 @@
# Design: Admin Test Checklist
**Date:** 2026-05-24
**Status:** Approved
**Updated:** 2026-05-24 (v2 — 3-state status + error comments)
## Summary
Админ-страница с чек-листом для ручного тестирования функционала сайта. Каждая проверка имеет описание действия и ожидаемый результат. Отметка сохраняется в SQLite с автоматической датой. Поддерживает 3 состояния: пройдено / не пройдено (с комментарием об ошибке) / не проверено.
## Architecture
### Data Model
Модель `ChecklistResult` в Prisma schema:
```prisma
model ChecklistResult {
id String @id @default(cuid())
itemKey String @unique // уникальный ключ проверки
passed Boolean // true = пройдено, false = не пройдено
comment String? // описание ошибки (при passed: false)
checkedAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
### Elements Source
Элементы чек-листа — хардкод в `shared/constants/test-checklist-items.js` (JS + .d.ts). Версионируются с кодом.
Структура элемента:
```ts
{
key: string // уникальный идентификатор
section: string // раздел (группировка)
action: string // что сделать
expectedResult: string // что должно произойти
}
```
### API
**Server route:** `server/src/routes/api/admin/test-checklist.js`
- `GET /api/admin/test-checklist` — возвращает все элементы + их результаты из БД (включая `comment`)
- `PATCH /api/admin/test-checklist` — body: `{ itemKey, passed, comment? }`, обновляет/создаёт результат, `checkedAt = now()`. При `passed: true` комментарий очищается.
- `POST /api/admin/test-checklist/reset` — сбрасывает все результаты (удаляет записи)
Только для админов.
### Frontend
**Page:** `client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx`
- Аккордеон по разделам (MUI Accordion)
- Каждая строка: 3-состоятельный статус (✅ пройдено / ❌ не пройдено / ⬜ не проверено), колонка "Действие", колонка "Ожидаемый результат", колонка "Комментарий" (показывает текст ошибки с tooltip), дата проверки
- При выборе «Не пройдено» — открывается диалог с обязательным полем «Описание ошибки» (textarea)
- Диалог: заголовок «Описание ошибки — {название проверки}», placeholder «Опишите, что пошло не так...», кнопки «Отмена» / «Сохранить», валидация — поле обязательно при ❌
- Кнопка «Сбросить все» для полного ре-теста
- `@tanstack/react-query` для данных, `apiClient` для запросов
**Navigation:** новый пункт в `AdminLayoutPage` — «Тест-чеклист» с иконкой `ClipboardCheck`, роут `/admin/test-checklist`
## Sections & Items
### Авторизация (auth)
- `auth.register-email` — Регистрация по email → код приходит, аккаунт создаётся
- `auth.login-password` — Вход по паролю → корректный пускает, неправильный — ошибка
- `auth.oauth-vk` — OAuth VK → редирект, callback, авторизация
- `auth.oauth-yandex` — OAuth Yandex → редирект, callback, авторизация
- `auth.reset-password` — Сброс пароля → письмо приходит, ссылка работает
- `auth.logout` — Выход → сессия очищается
### Каталог и товары (catalog)
- `catalog.homepage` — Главная → слайдер грузится, товары отображаются
- `catalog.filters` — Фильтры → по категории, цене, материалам работают
- `catalog.product-page` — Страница товара → фото, описание, цена, кнопка "В корзину"
- `catalog.seo` — SEO → title, meta, slug корректные
### Корзина (cart)
- `cart.add` — Добавление товара → счётчик обновляется
- `cart.change-qty` — Изменение количества → пересчёт суммы
- `cart.remove` — Удаление → товар убирается, сумма пересчитывается
### Чекаут (checkout)
- `checkout.address` — Выбор адреса → из сохранённых / новый
- `checkout.delivery` — Выбор доставки → Почта, OZON, Яндекс, 5post
- `checkout.payment` — Выбор оплаты → онлайн / при получении
- `checkout.comment` — Комментарий к заказу → поле работает
- `checkout.create` — Создание заказа → заказ создаётся, статус DRAFT
### Оплата (payment)
- `payment.yookassa` — ЮKassa → редирект на оплату, webhook обрабатывается
- `payment.status` — Статус платежа → обновляется после webhook
### Профиль пользователя (profile)
- `profile.avatar` — Аватар → загрузка, отображение, удаление
- `profile.settings` — Настройки → email, имя, способы входа
- `profile.addresses` — Адреса → добавление, редактирование, удаление, по умолчанию
- `profile.orders` — Заказы → список, детали, статусы
- `profile.messages` — Сообщения по заказу → отправка, получение, read state
- `profile.notifications` — Уведомления → вкл/выкл каналы
- `profile.delete-account` — Удаление аккаунта → данные удаляются
### Админ — Товары (admin-products)
- `admin-products.list` — Список → пагинация, поиск
- `admin-products.create` — Создание → все поля, загрузка фото, публикация
- `admin-products.edit` — Редактирование → изменения сохраняются
- `admin-products.delete` — Удаление → товар удаляется
- `admin-products.images` — Изображения → добавление, сортировка, удаление
### Админ — Категории (admin-categories)
- `admin-categories.crud` — CRUD категорий, сортировка
### Админ — Заказы (admin-orders)
- `admin-orders.list` — Список → фильтрация по статусу, внимание
- `admin-orders.details` — Детали → состав, статус, смена статуса
- `admin-orders.messages` — Сообщения → ответ пользователю
### Админ — Отзывы (admin-reviews)
- `admin-reviews.list` — Список → pending/approved/rejected
- `admin-reviews.moderate` — Модерация → approve/reject
### Админ — Пользователи (admin-users)
- `admin-users.list` — Список → email, дата регистрации
- `admin-users.orders` — Просмотр заказов пользователя
### Админ — Галерея (admin-gallery)
- `admin-gallery.upload` — Загрузка, удаление, использование в слайдере
### Админ — Настройки (admin-settings)
- `admin-settings.notifications` — Уведомления → email, telegram
### Инфо-страницы (info)
- `info.pages` — Доставка, оплата, как заказать, статусы заказов
- `info.legal` — Политика конфиденциальности, условия использования
### SSE / Realtime (sse)
- `sse.notifications` — Уведомления приходят в реальном времени
## Implementation Plan
1. Prisma migration — добавить модель `ChecklistResult`
2. Shared constants — `test-checklist-items.ts`
3. Server API route — GET/PATCH/POST
4. Client page — `AdminTestChecklistPage`
5. Client routing — добавить в `AdminLayoutPage`
6. Register route in `client/src/app/routes/index.tsx`
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "ChecklistResult" (
"id" TEXT NOT NULL PRIMARY KEY,
"itemKey" TEXT NOT NULL,
"passed" BOOLEAN NOT NULL,
"checkedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "ChecklistResult_itemKey_key" ON "ChecklistResult"("itemKey");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ChecklistResult" ADD COLUMN "comment" TEXT;
+10
View File
@@ -340,3 +340,13 @@ model NotificationLog {
@@index([status, createdAt])
@@index([userId, createdAt])
}
/// Результат ручной проверки тест-чеклиста
model ChecklistResult {
id String @id @default(cuid())
itemKey String @unique
passed Boolean
comment String?
checkedAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+2
View File
@@ -1,5 +1,6 @@
import { mapProductForApi, parseMaterialsInput, slugify } from './api/_product-helpers.js'
import { registerAdminNotificationRoutes } from './api/admin/notifications.js'
import { registerAdminTestChecklistRoutes } from './api/admin/test-checklist.js'
import { registerAdminCategoryRoutes } from './api/admin-categories.js'
import { registerAdminGalleryRoutes } from './api/admin-gallery.js'
import { registerAdminOrderRoutes } from './api/admin-orders.js'
@@ -31,6 +32,7 @@ export async function registerApiRoutes(fastify) {
await registerAdminReviewRoutes(fastify)
await registerAdminUserRoutes(fastify)
await registerAdminNotificationRoutes(fastify)
await registerAdminTestChecklistRoutes(fastify)
await registerAdminProfileRoutes(fastify)
await registerAuthRoutes(fastify)
@@ -0,0 +1,38 @@
import { prisma } from '../../../lib/prisma.js'
export async function registerAdminTestChecklistRoutes(fastify) {
fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
const results = await prisma.checklistResult.findMany()
const resultMap = {}
for (const r of results) {
resultMap[r.itemKey] = { passed: r.passed, comment: r.comment, checkedAt: r.checkedAt.toISOString() }
}
return { results: resultMap }
})
fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
const { itemKey, passed, comment } = request.body || {}
if (!itemKey || typeof passed !== 'boolean') {
return reply.code(400).send({ error: 'itemKey и passed (boolean) обязательны' })
}
if (comment !== undefined && comment !== null && typeof comment !== 'string') {
return reply.code(400).send({ error: 'comment должен быть строкой' })
}
if (comment !== undefined && comment !== null && comment.length > 2000) {
return reply.code(400).send({ error: 'Комментарий слишком длинный (макс. 2000 символов)' })
}
const result = await prisma.checklistResult.upsert({
where: { itemKey },
create: { itemKey, passed, comment: passed ? null : comment || null },
update: { passed, comment: passed ? null : comment ?? undefined, checkedAt: new Date() },
})
return { result: { itemKey: result.itemKey, passed: result.passed, comment: result.comment, checkedAt: result.checkedAt.toISOString() } }
})
fastify.post('/api/admin/test-checklist/reset', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
await prisma.checklistResult.deleteMany({})
return { ok: true }
})
}
+8
View File
@@ -0,0 +1,8 @@
export interface TestChecklistItem {
key: string;
section: string;
action: string;
expectedResult: string;
}
export declare const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[];
+304
View File
@@ -0,0 +1,304 @@
export const TEST_CHECKLIST_ITEMS = Object.freeze([
// Авторизация
{
key: "auth.register-email",
section: "Авторизация",
action: "Зарегистрироваться по email",
expectedResult: "Код приходит на почту, аккаунт создаётся",
},
{
key: "auth.login-password",
section: "Авторизация",
action: "Войти по паролю",
expectedResult: "Корректный пароль пускает, неправильный — ошибка",
},
{
key: "auth.oauth-vk",
section: "Авторизация",
action: "Войти через OAuth VK",
expectedResult: "Редирект на VK, callback, авторизация успешна",
},
{
key: "auth.oauth-yandex",
section: "Авторизация",
action: "Войти через OAuth Yandex",
expectedResult: "Редирект на Yandex, callback, авторизация успешна",
},
{
key: "auth.reset-password",
section: "Авторизация",
action: "Сбросить пароль",
expectedResult: "Письмо приходит, ссылка работает, пароль меняется",
},
{
key: "auth.logout",
section: "Авторизация",
action: "Выйти из аккаунта",
expectedResult: "Сессия очищается, редирект на страницу входа",
},
// Каталог и товары
{
key: "catalog.homepage",
section: "Каталог и товары",
action: "Открыть главную страницу",
expectedResult: "Слайдер грузится, товары отображаются",
},
{
key: "catalog.filters",
section: "Каталог и товары",
action: "Применить фильтры",
expectedResult: "Фильтры по категории, цене, материалам работают",
},
{
key: "catalog.product-page",
section: "Каталог и товары",
action: "Открыть страницу товара",
expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются',
},
{
key: "catalog.seo",
section: "Каталог и товары",
action: "Проверить SEO-метаданные",
expectedResult: "Title, meta, slug корректные",
},
// Корзина
{
key: "cart.add",
section: "Корзина",
action: "Добавить товар в корзину",
expectedResult: "Счётчик корзины обновляется",
},
{
key: "cart.change-qty",
section: "Корзина",
action: "Изменить количество товара",
expectedResult: "Сумма пересчитывается",
},
{
key: "cart.remove",
section: "Корзина",
action: "Удалить товар из корзины",
expectedResult: "Товар убирается, сумма пересчитывается",
},
// Чекаут
{
key: "checkout.address",
section: "Чекаут",
action: "Выбрать адрес доставки",
expectedResult: "Можно выбрать из сохранённых или добавить новый",
},
{
key: "checkout.delivery",
section: "Чекаут",
action: "Выбрать способ доставки",
expectedResult: "Почта, OZON, Яндекс, 5post — доступны",
},
{
key: "checkout.payment",
section: "Чекаут",
action: "Выбрать способ оплаты",
expectedResult: "Онлайн / при получении — доступны",
},
{
key: "checkout.comment",
section: "Чекаут",
action: "Добавить комментарий к заказу",
expectedResult: "Поле работает, текст сохраняется",
},
{
key: "checkout.create",
section: "Чекаут",
action: "Создать заказ",
expectedResult: "Заказ создаётся, статус DRAFT",
},
// Оплата
{
key: "payment.yookassa",
section: "Оплата",
action: "Оплатить через ЮKassa",
expectedResult: "Редирект на оплату, webhook обрабатывается",
},
{
key: "payment.status",
section: "Оплата",
action: "Проверить статус платежа",
expectedResult: "Статус обновляется после webhook",
},
// Профиль пользователя
{
key: "profile.avatar",
section: "Профиль пользователя",
action: "Управление аватаром",
expectedResult: "Загрузка, отображение, удаление работают",
},
{
key: "profile.settings",
section: "Профиль пользователя",
action: "Изменить настройки профиля",
expectedResult: "Email, имя, способы входа обновляются",
},
{
key: "profile.addresses",
section: "Профиль пользователя",
action: "Управление адресами",
expectedResult: "Добавление, редактирование, удаление, по умолчанию",
},
{
key: "profile.orders",
section: "Профиль пользователя",
action: "Просмотр заказов",
expectedResult: "Список, детали, статусы отображаются",
},
{
key: "profile.messages",
section: "Профиль пользователя",
action: "Сообщения по заказу",
expectedResult: "Отправка, получение, read state работают",
},
{
key: "profile.notifications",
section: "Профиль пользователя",
action: "Настройки уведомлений",
expectedResult: "Вкл/выкл каналов работают",
},
{
key: "profile.delete-account",
section: "Профиль пользователя",
action: "Удалить аккаунт",
expectedResult: "Данные удаляются",
},
// Админ — Товары
{
key: "admin-products.list",
section: "Админ — Товары",
action: "Открыть список товаров",
expectedResult: "Пагинация, поиск работают",
},
{
key: "admin-products.create",
section: "Админ — Товары",
action: "Создать товар",
expectedResult:
"Все поля сохраняются, фото загружаются, публикация работает",
},
{
key: "admin-products.edit",
section: "Админ — Товары",
action: "Редактировать товар",
expectedResult: "Изменения сохраняются",
},
{
key: "admin-products.delete",
section: "Админ — Товары",
action: "Удалить товар",
expectedResult: "Товар удаляется",
},
{
key: "admin-products.images",
section: "Админ — Товары",
action: "Управление изображениями товара",
expectedResult: "Добавление, сортировка, удаление работают",
},
// Админ — Категории
{
key: "admin-categories.crud",
section: "Админ — Категории",
action: "CRUD категорий",
expectedResult: "Создание, редактирование, удаление, сортировка работают",
},
// Админ — Заказы
{
key: "admin-orders.list",
section: "Админ — Заказы",
action: "Открыть список заказов",
expectedResult: "Фильтрация по статусу, внимание отображается",
},
{
key: "admin-orders.details",
section: "Админ — Заказы",
action: "Открыть детали заказа",
expectedResult: "Состав, статус, смена статуса работают",
},
{
key: "admin-orders.messages",
section: "Админ — Заказы",
action: "Ответить на сообщение заказа",
expectedResult: "Сообщение отправляется пользователю",
},
// Админ — Отзывы
{
key: "admin-reviews.list",
section: "Админ — Отзывы",
action: "Открыть список отзывов",
expectedResult: "Фильтрация pending/approved/rejected работает",
},
{
key: "admin-reviews.moderate",
section: "Админ — Отзывы",
action: "Модерировать отзыв",
expectedResult: "Approve/reject работают",
},
// Админ — Пользователи
{
key: "admin-users.list",
section: "Админ — Пользователи",
action: "Открыть список пользователей",
expectedResult: "Email, дата регистрации отображаются",
},
{
key: "admin-users.orders",
section: "Админ — Пользователи",
action: "Просмотр заказов пользователя",
expectedResult: "Заказы пользователя отображаются",
},
// Админ — Галерея
{
key: "admin-gallery.upload",
section: "Админ — Галерея",
action: "Управление галереей",
expectedResult: "Загрузка, удаление, использование в слайдере работают",
},
// Админ — Настройки
{
key: "admin-settings.notifications",
section: "Админ — Настройки",
action: "Настройки уведомлений админа",
expectedResult: "Email, telegram настраиваются",
},
// Инфо-страницы
{
key: "info.pages",
section: "Инфо-страницы",
action: "Открыть инфо-страницы",
expectedResult:
"Доставка, оплата, как заказать, статусы заказов отображаются",
},
{
key: "info.legal",
section: "Инфо-страницы",
action: "Открыть юридические страницы",
expectedResult:
"Политика конфиденциальности, условия использования отображаются",
},
// SSE / Realtime
{
key: "sse.notifications",
section: "SSE / Realtime",
action: "Проверить SSE-уведомления",
expectedResult: "Уведомления приходят в реальном времени",
},
]);
File diff suppressed because it is too large Load Diff
@@ -1,157 +0,0 @@
---
title: "Требования Роскомнадзора к сайтам 2026: чек-лист для бизнеса"
source: "https://www.klerk.ru/blogs/roskom24/650389/#chapter--4-cookie-uvedomlenie-i-politika"
author:
- "[[Закон и бизнес | Онлайн услуги 24]]"
published: 2025-06-10
created: 2026-05-23
description: "С 30 мая 2025 года в России вступили в силу изменения в законодательство о персональных данных. Для бизнеса — это не просто очередная «формальность», а вопрос безопасности и выживания."
tags:
- "clippings"
---
Роскомнадзор усилил контроль за сайтами, включая автоматическую проверку с использованием ИИ. Теперь даже незначительные, по мнению бизнеса, нарушения могут привести **к штрафам до 18 миллионов рублей или блокировке сайта**.
Если у вас есть сайт, вы — оператор персональных данных. А значит, обязаны соблюдать [ФЗ-152 «О персональных данных»](https://www.klerk.ru/cdoc/view/federalnyj-zakon-ot-27072006-no-152-fz-o-personalnyh-dannyh/). Даже если вы ИП, самозанятый или оказываете услуги онлайн. Ниже — актуальный чек-лист на 2026 год, который поможет не попасть под штраф и не дать конкурентам «закопать» ваш бизнес через жалобу в Роскомнадзор.
## ✅ 1. Политика обработки персональных данных на сайте
### Что это
Официальный документ, размещенный на сайте (*обычно в подвале*), который описывает:
- какие данные вы собираете;
- как их обрабатываете;
- кому передаете и на каких основаниях.
### Требования 2026
- Обязательно актуальная редакция.
- Полный перечень обрабатываемых данных (*имя, email, телефон, cookies и т.д.*).
- Указание целей и оснований обработки.
- Контактные данные оператора.
- Ссылки на формы согласия и порядок отзыва.
📌 **Ошибка №1** — скачать шаблон и забыть про него. Политика должна соответствовать именно вашему бизнесу и интеграциям на сайте.
## ✅ 2. Согласие на обработку персональных данных
### Где должно быть
- во всех формах на сайте: заявки, обратная связь, регистрация, квизы, покупка, консультации;
- при подписке на рассылку;
- в онлайн-чате, если сохраняются данные.
### Требования 2026
- Согласие должно быть **добровольным, конкретным, информированным и однозначным**.
- Включает: ФИО, перечень данных, цель обработки, срок хранения, право отзыва.
- Отдельное согласие на передачу данных третьим лицам (*например, CRM-системам*).
- Техническая реализация: **отдельный чекбокс с обязательной активацией**, а не просто фраза «нажимая кнопку, вы соглашаетесь».
📌 **Ошибка №2** — отсутствие чекбокса или невидимый текст, отсутствие Log файлов позволяющих доказать получение согласия на обработку персональных данных от пользователя сайта.
## ✅ 3. Уведомление Роскомнадзора о начале обработки персональных данных
### Кто обязан
Любой, кто собирает ПДн через сайт — даже ИП и самозанятые.
### Требования 2026
- До начала обработки нужно подать уведомление через портал Роскомнадзора.
- Указать все сведения: цели, способы обработки, меры безопасности, перечень используемых информационных систем.
- Отдельно — факт трансграничной передачи, если используете иностранные сервисы.
📌 **Ошибка №3** — не уведомили Роскомнадзор, потому что «сайт только визитка». Даже форма обратной связи — уже обработка ПДн.
## ✅ 5. Юридическая информация в подвале сайта
Что должно быть ([*ч. 2 ст. 10 ФЗ № 149-ФЗ «Об информации, информационных технологиях и о защите информации»*](https://www.klerk.ru/cdoc/view/federalnyj-zakon-ot-27072006-no-149-fz-ob-informacii-informacionnyh-tehnologiah-i-o-zasite-informacii/stata-10-rasprostranenie-informacii-ili-predostavlenie-informacii/#p_64595)):
- Полное наименование владельца сайта.
- Адрес места нахождения.
- Актуальные контактные данные.
### Почему это важно
Размещение недостоверных сведений может привести к привлечению к административной ответственности по [статье 14.4 КоАП](https://www.klerk.ru/cdoc/view/kodeks-ob-administrativnyh-pravonaruseniah-koap-rf/stata-144-prodaza-tovarov-vypolnenie-rabot-libo-okazanie-naseleniu-uslug-nenadlezasego-kacestva-ili-s-naruseniem-ustanovlennyh-zakonodatelstvom-rossijskoj-federacii-trebovanij/) (*нарушение законодательства о рекламе*), а также к гражданско-правовой ответственности за причиненный ущерб.
📌 **Ошибка №5** — указание только бренда или торговой марки без юр. лица.
## ✅ 6. Российский хостинг и запрет трансграничной передачи
### Суть
Использование **иностранных серверов и облаков** приравнивается к трансграничной передаче ПДн.
### Требования
- Хостинг сайта — только на серверах, физически размещенных в России.
- Подтверждение от хостинг-провайдера.
- Запрет на хранение ПДн в Google Drive, Notion, Dropbox и т.д.
- Meta <sup>1</sup> Pixel, Google Analytics, сайты размещенные на иностранных хостингах — повод для штрафа, если не отражены в документах.
📌 **Ошибка №7** — сайт на Tilda или REG.RU, но физически размещен в Европе. Это нарушение.
Материалы по теме[Топ вопросов и ответов про работу с персональными данными в 2025 году](https://www.klerk.ru/buh/articles/660617/?utm_source=klerk&utm_medium=article&utm_campaign=recommendation&utm_content=blocklinks&utm_term=650389)
[
Главные изменения в законе о персональных данных с 1 сентября 2025
](https://www.klerk.ru/buh/articles/660820/?utm_source=klerk&utm_medium=article&utm_campaign=recommendation&utm_content=blocklinks&utm_term=650389)[
152-ФЗ о персональных данных: требования закона для бизнеса в 2026 году
](https://www.klerk.ru/blogs/roskom24/674017/?utm_source=klerk&utm_medium=article&utm_campaign=recommendation&utm_content=blocklinks&utm_term=650389)
## ✅ 8. Проверка Роскомнадзора: автоматизированная, быстрая, без предупреждения
### Как работает
- Искусственный интеллект сканирует сайт 24/7.
- Проверяет не только текст, но и **код сайта, скрытые скрипты, cookie, формы**.
- Не требует предварительного уведомления.
- Фиксация нарушений → **предписание или моментальный штраф**.
📌 **Важно**: проверить сайт глазами — недостаточно. Нарушения могут быть «внутри» — в интеграциях, скриптах и DOM-структуре.
## ⚠️ Жалобы конкурентов — новый инструмент давления
- Предприниматели уже используют жалобы в Роскомнадзор как способ атаковать конкурентов.
- Роскомнадзор обязан реагировать на любую жалобу, даже анонимную.
- Уже есть случаи блокировки сайтов и штрафов по жалобе «доброжелателей».
📌 **Вывод** — не дать конкурентам повода. Даже мелкая недоработка — риск для бизнеса.
## 🛠 Что делать бизнесу уже сейчас
### Шаги
1. Провести аудит сайта (*юридический + технический*).
2. Проверить наличие и актуальность всех обязательных документов.
3. Зарегистрироваться в Роскомнадзоре как оператор ПДн.
4. Обновить Политику ПДн и Cookie-согласие.
5. Убедиться, что сайт размещен на российских серверах.
6. Устранить трансграничные риски (*зарубежные сервисы, скрипты*).
7. Назначить ответственного за ПДн внутри компании.
## 💼 Как мы можем помочь
Сервис [«Роском 24»](https://roskom24.ru/?utm_source=klerkru-blog&utm_medium=trebovaniya_roskomnadzora_k_saitam) — это команда юристов и технических специалистов, которые:
- Проводят аудит сайта на соответствие [ФЗ-152](https://www.klerk.ru/cdoc/view/federalnyj-zakon-ot-27072006-no-152-fz-o-personalnyh-dannyh/).
- Подготавливают полный комплект документов.
- Помогают зарегистрироваться в Роскомнадзоре.
- Настраивают cookie-уведомления и формы согласия.
- Защищают бизнес от штрафов и блокировок.
## 📌 Итог
В 2026 году **невозможно заниматься бизнесом онлайн и игнорировать закон о персональных данных**. Сайт — это уже зона юридической ответственности. И чем раньше вы проведете аудит и приведете все в порядок, тем больше шансов избежать штрафов, блокировок и атак конкурентов.
👉 Проверьте свой сайт прямо сейчас. Или [доверьтесь профессионалам](https://roskom24.ru/?utm_source=klerkru-blog&utm_medium=trebovaniya_roskomnadzora_k_saitam).
*Реклама: ООО «ОНЛАЙН УСЛУГИ 24», ИНН 7751227590, erid: 2W5zFJLb1J8*
1. Деятельность компании Meta Platforms Inc. (Facebook и Instagram) на территории РФ запрещена