diff --git a/apps/api/prisma/migrations/20260324141120_add_changelog_field/migration.sql b/apps/api/prisma/migrations/20260324141120_add_changelog_field/migration.sql new file mode 100644 index 0000000..6efe454 --- /dev/null +++ b/apps/api/prisma/migrations/20260324141120_add_changelog_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Block" ADD COLUMN "changelog" JSONB NOT NULL DEFAULT '[]'; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index d186fcb..daa377e 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -46,6 +46,7 @@ model Block { name String version String isInPreview Boolean @default(false) + changelog Json @default("[]") updatedAt DateTime @updatedAt createdAt DateTime @default(now()) } diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index 960a2a9..7da2d1c 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -6,21 +6,120 @@ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); const BLOCKS = [ - { path: '/components/navigation', name: 'Шапка / Навигация', version: 'v1.0', isInPreview: true }, - { path: '/blocks/hero', name: 'Hero-баннер', version: 'v1.1', isInPreview: true }, - { path: '/blocks/ceo', name: 'Вводный текст (CEO-блок)', version: 'v0.1', isInPreview: false }, - { path: '/blocks/doctors', name: 'Наши врачи', version: 'v1.1', isInPreview: true }, - { path: '/blocks/reviews', name: 'Отзывы', version: 'v0.1', isInPreview: false }, - { path: '/blocks/contact-forms', name: 'Формы записи', version: 'v0.1', isInPreview: false }, - { path: '/blocks/news', name: 'Новости', version: 'v0.1', isInPreview: false }, - { path: '/blocks/contact', name: 'Подвал / Контакт', version: 'v0.1', isInPreview: false }, + { + path: '/components/navigation', + name: 'Шапка / Навигация', + version: 'v1.0', + isInPreview: true, + changelog: [ + { version: 'v1.0', date: '23.03.2026', changes: ['Топ-бар, логотип, главное меню из 8 пунктов'] }, + ], + }, + { + path: '/blocks/hero', + name: 'Hero-баннер', + version: 'v1.2', + isInPreview: true, + changelog: [ + { version: 'v1.2', date: '24.03.2026', changes: [ + 'H1: цвет #cb9768, размер 36px (было ~20px #53514e)', + 'Заголовок баннера: 22px #333 (было 16px #111827)', + 'CTA-кнопка: pill-стиль (было outline)', + 'Дефис в H1: «–» → «-»', + ]}, + { version: 'v1.1', date: '23.03.2026', changes: [ + 'Единый фон #f9f4e7 (ранее разбит на две зоны)', + 'Реальное фото врача с пациентом', + ]}, + ], + }, + { + path: '/blocks/ceo', + name: 'Вводный текст (CEO-блок)', + version: 'v1.0', + isInPreview: false, + changelog: [ + { version: 'v1.0', date: '23.03.2026', changes: ['Текст специализации клиники, вопросы-стимулы'] }, + ], + }, + { + path: '/blocks/doctors', + name: 'Наши врачи', + version: 'v1.2', + isInPreview: true, + changelog: [ + { version: 'v1.2', date: '24.03.2026', changes: [ + 'H2: 36px #000000 (было ~30px #111827)', + 'line-height: 38px', + ]}, + { version: 'v1.1', date: '23.03.2026', changes: [ + '6 реальных фото врачей с сайта', + 'Статистика без фона, только border-bottom #60959c', + ]}, + ], + }, + { + path: '/blocks/reviews', + name: 'Отзывы', + version: 'v1.1', + isInPreview: true, + changelog: [ + { version: 'v1.1', date: '24.03.2026', changes: [ + 'H2: 36px #000000 (было ~20px #111827)', + 'line-height: 38px', + ]}, + { version: 'v1.0', date: '23.03.2026', changes: [ + 'Карусель отзывов: кавычка, текст, стрелки', + ]}, + ], + }, + { + path: '/blocks/contact-forms', + name: 'Формы записи', + version: 'v1.1', + isInPreview: true, + changelog: [ + { version: 'v1.1', date: '24.03.2026', changes: [ + 'H2: 36px #000000', + 'Фон формы 1: #b8e6ed → #d4f6f8', + 'Фон формы 2: #ffffff → #d4f6f8 (обе формы на одном фоне)', + ]}, + { version: 'v1.0', date: '23.03.2026', changes: [ + 'Две формы записи: «Будьте здоровы!» и «Узнайте стоимость операции»', + ]}, + ], + }, + { + path: '/blocks/news', + name: 'Новости', + version: 'v1.1', + isInPreview: true, + changelog: [ + { version: 'v1.1', date: '24.03.2026', changes: [ + 'H2: 36px #000000', + 'Фон секции: #fff → #f2fee6 (светло-зелёный)', + ]}, + { version: 'v1.0', date: '23.03.2026', changes: [ + '4 карточки новостей в ряд, кнопка «Все новости»', + ]}, + ], + }, + { + path: '/blocks/contact', + name: 'Подвал / Контакт', + version: 'v1.0', + isInPreview: false, + changelog: [ + { version: 'v1.0', date: '23.03.2026', changes: ['4 колонки ссылок, адрес, часы работы'] }, + ], + }, ]; async function main() { for (const block of BLOCKS) { await prisma.block.upsert({ where: { path: block.path }, - update: {}, + update: { version: block.version, isInPreview: block.isInPreview, changelog: block.changelog }, create: block, }); } diff --git a/apps/api/src/blocks/blocks.controller.ts b/apps/api/src/blocks/blocks.controller.ts index c979fb0..f3b59bb 100644 --- a/apps/api/src/blocks/blocks.controller.ts +++ b/apps/api/src/blocks/blocks.controller.ts @@ -18,7 +18,7 @@ export class BlocksController { @Patch('by-path') update( @Query('path') path: string, - @Body() body: { version?: string; isInPreview?: boolean }, + @Body() body: { version?: string; isInPreview?: boolean; changelog?: object[] }, ) { return this.blocks.update(path, body); } diff --git a/apps/api/src/blocks/blocks.service.ts b/apps/api/src/blocks/blocks.service.ts index bae1ea7..01ab3ba 100644 --- a/apps/api/src/blocks/blocks.service.ts +++ b/apps/api/src/blocks/blocks.service.ts @@ -17,7 +17,7 @@ export class BlocksService { }); } - update(path: string, data: { version?: string; isInPreview?: boolean }) { + update(path: string, data: { version?: string; isInPreview?: boolean; changelog?: object[] }) { return this.prisma.block.update({ where: { path }, data }); } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index a7ce6d1..b179239 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,7 +4,7 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors({ origin: 'http://localhost:3001' }); + app.enableCors({ origin: [/^http:\/\/localhost:\d+$/] }); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/apps/web/app/blocks/contact-forms/page.tsx b/apps/web/app/blocks/contact-forms/page.tsx index 5b81e07..ec862cc 100644 --- a/apps/web/app/blocks/contact-forms/page.tsx +++ b/apps/web/app/blocks/contact-forms/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; +import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock"; export const metadata: Metadata = { @@ -50,6 +51,18 @@ const LLM_FORMS_TEXT = ` ✕ Не убирать чекбокс согласия `.trim(); +const CHANGELOG: ChangelogEntry[] = [ + { + version: "v1.1", + date: "24.03.2026", + changes: [ + "H2: размер на 36px, цвет на #000000, line-height 38px", + "Фон формы 1: с #b8e6ed на #d4f6f8", + "Фон формы 2: с #ffffff на #d4f6f8 (обе формы на одном фоне)", + ], + }, +]; + export default function ContactFormsPage() { return (
@@ -64,7 +77,7 @@ export default function ContactFormsPage() {

Формы записи

- +

Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.

@@ -112,21 +125,6 @@ export default function ContactFormsPage() {
- {/* Changelog */} -
-

- История версий -

-
-

v1.1 — 24.03.2026

- -
-
- {/* LLM блок */} diff --git a/apps/web/app/blocks/doctors/page.tsx b/apps/web/app/blocks/doctors/page.tsx index a9140c8..eaede90 100644 --- a/apps/web/app/blocks/doctors/page.tsx +++ b/apps/web/app/blocks/doctors/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock"; import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; +import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; export const metadata: Metadata = { title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -45,6 +46,25 @@ const LLM_DOCTORS_TEXT = ` ✕ Не убирать статистику `.trim(); +const CHANGELOG: ChangelogEntry[] = [ + { + version: "v1.2", + date: "24.03.2026", + changes: [ + "H2: размер с ~30px на 36px, цвет с #111827 на #000000", + "H2 line-height: 38px", + ], + }, + { + version: "v1.1", + date: "23.03.2026", + changes: [ + "6 реальных фото врачей с сайта", + "Статистика без фона, только border-bottom #60959c", + ], + }, +]; + export default function DoctorsBlockPage() { return (
@@ -59,7 +79,7 @@ export default function DoctorsBlockPage() {

Блок «Наши врачи»

- +

Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.

@@ -101,29 +121,6 @@ export default function DoctorsBlockPage() {
- {/* Changelog */} -
-

- История версий -

-
-
-

v1.2 — 24.03.2026

-
    -
  • H2: размер с ~30px на 36px, цвет с #111827 на #000000
  • -
  • H2 line-height: 38px
  • -
-
-
-

v1.1 — 23.03.2026

-
    -
  • 6 реальных фото врачей с сайта
  • -
  • Статистика без фона, только border-bottom #60959c
  • -
-
-
-
- {/* LLM блок */} diff --git a/apps/web/app/blocks/hero/page.tsx b/apps/web/app/blocks/hero/page.tsx index a94bcdb..c3682c3 100644 --- a/apps/web/app/blocks/hero/page.tsx +++ b/apps/web/app/blocks/hero/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock"; import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; +import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; export const metadata: Metadata = { title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -50,6 +51,27 @@ const LLM_HERO_TEXT = ` ✕ Не убирать три пункта с галочками `.trim(); +const CHANGELOG: ChangelogEntry[] = [ + { + version: "v1.2", + date: "24.03.2026", + changes: [ + "H1: цвет исправлен с #53514e на #cb9768, размер с ~20px на 36px", + "Заголовок баннера: размер с ~16px на 22px, цвет с #111827 на #333333", + "CTA-кнопка: стиль изменён с outline на pill (фон #E9E4D4, radius 25px)", + "Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)", + ], + }, + { + version: "v1.1", + date: "23.03.2026", + changes: [ + "Единый фон #f9f4e7 (ранее был разбит на две зоны)", + "Реальное фото врача с пациентом", + ], + }, +]; + export default function HeroPage() { return (
@@ -64,7 +86,7 @@ export default function HeroPage() {

Hero-баннер

- +

Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "} #f9f4e7. @@ -138,31 +160,6 @@ export default function HeroPage() {

- {/* Changelog */} -
-

- История версий -

-
-
-

v1.2 — 24.03.2026

-
    -
  • H1: цвет исправлен с #53514e на #cb9768, размер с ~20px на 36px
  • -
  • Заголовок баннера: размер с ~16px на 22px, цвет с #111827 на #333333
  • -
  • CTA-кнопка: стиль изменён с outline на pill (фон #E9E4D4, radius 25px)
  • -
  • Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)
  • -
-
-
-

v1.1 — 23.03.2026

-
    -
  • Единый фон #f9f4e7 (ранее был разбит на две зоны)
  • -
  • Реальное фото врача с пациентом
  • -
-
-
-
- {/* LLM блок */} diff --git a/apps/web/app/blocks/news/page.tsx b/apps/web/app/blocks/news/page.tsx index 44f5bb7..4ba826a 100644 --- a/apps/web/app/blocks/news/page.tsx +++ b/apps/web/app/blocks/news/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; +import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; import { NewsBlock } from "@/components/blocks/NewsBlock"; export const metadata: Metadata = { @@ -67,6 +68,17 @@ const LLM_NEWS_TEXT = ` ✕ Не добавлять описание/анонс в карточку `.trim(); +const CHANGELOG: ChangelogEntry[] = [ + { + version: "v1.1", + date: "24.03.2026", + changes: [ + "H2: размер на 36px, цвет на #000000, line-height 38px", + "Фон секции: с белого на #f2fee6 (светло-зелёный, как на реальном сайте)", + ], + }, +]; + export default function NewsBlockPage() { return (
@@ -81,7 +93,7 @@ export default function NewsBlockPage() {

Блок «Новости»

- +

Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка), кнопка «Все новости». @@ -132,20 +144,6 @@ export default function NewsBlockPage() {

- {/* Changelog */} -
-

- История версий -

-
-

v1.1 — 24.03.2026

-
    -
  • H2: размер на 36px, цвет на #000000, line-height 38px
  • -
  • Фон секции: с белого на #f2fee6 (светло-зелёный, как на реальном сайте)
  • -
-
-
- {/* LLM блок */} diff --git a/apps/web/app/blocks/reviews/page.tsx b/apps/web/app/blocks/reviews/page.tsx index c149e6b..3ff4fea 100644 --- a/apps/web/app/blocks/reviews/page.tsx +++ b/apps/web/app/blocks/reviews/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; +import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; import { ReviewsBlock } from "@/components/blocks/ReviewsBlock"; export const metadata: Metadata = { @@ -52,6 +53,16 @@ const LLM_REVIEWS_TEXT = ` ✕ Не убирать навигационные стрелки `.trim(); +const CHANGELOG: ChangelogEntry[] = [ + { + version: "v1.1", + date: "24.03.2026", + changes: [ + "H2: размер с ~20px на 36px, цвет с #111827 на #000000, line-height 38px", + ], + }, +]; + export default function ReviewsBlockPage() { return (
@@ -66,7 +77,7 @@ export default function ReviewsBlockPage() {

Блок «Отзывы о нас»

- +

Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.

@@ -157,19 +168,6 @@ export default function ReviewsBlockPage() {
- {/* Changelog */} -
-

- История версий -

-
-

v1.1 — 24.03.2026

-
    -
  • H2: размер с ~20px на 36px, цвет с #111827 на #000000, line-height 38px
  • -
-
-
- {/* LLM блок */} diff --git a/apps/web/components/ui/BlockChangelog.tsx b/apps/web/components/ui/BlockChangelog.tsx new file mode 100644 index 0000000..72f2e70 --- /dev/null +++ b/apps/web/components/ui/BlockChangelog.tsx @@ -0,0 +1,41 @@ +"use client"; + +export interface ChangelogEntry { + version: string; + date: string; + changes: string[]; +} + +interface BlockChangelogProps { + changelog: ChangelogEntry[]; +} + +export function BlockChangelog({ changelog }: BlockChangelogProps) { + if (!changelog || changelog.length === 0) return null; + + return ( +
+

+ История версий +

+
+ {changelog.map((entry) => ( +
+

+ {entry.version} — {entry.date} +

+
    + {entry.changes.map((change, i) => ( +
  • {change}
  • + ))} +
+
+ ))} +
+
+ ); +} diff --git a/apps/web/components/ui/BlockMetaBar.tsx b/apps/web/components/ui/BlockMetaBar.tsx index 2dccd1e..3c8b6e1 100644 --- a/apps/web/components/ui/BlockMetaBar.tsx +++ b/apps/web/components/ui/BlockMetaBar.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { BlockChangelog, type ChangelogEntry } from "./BlockChangelog"; const LS_PREFIX = "bb-block-preview:"; @@ -21,15 +22,17 @@ interface BlockMeta { name: string; version: string; isInPreview: boolean; + changelog: ChangelogEntry[]; } interface BlockMetaBarProps { path: string; defaultVersion: string; defaultIsInPreview: boolean; + defaultChangelog?: ChangelogEntry[]; } -export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: BlockMetaBarProps) { +export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [] }: BlockMetaBarProps) { const [meta, setMeta] = useState(null); const [editing, setEditing] = useState(false); const [versionInput, setVersionInput] = useState(defaultVersion); @@ -55,7 +58,7 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block .catch(() => setApiDown(true)); }, [apiUrl, path]); - async function patch(data: { version?: string; isInPreview?: boolean }) { + async function patch(data: { version?: string; isInPreview?: boolean; changelog?: ChangelogEntry[] }) { if (!apiUrl) return null; const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, { method: 'PATCH', @@ -78,7 +81,6 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block async function togglePreview() { if (apiDown) { - // Fallback: toggle via localStorage const newVal = !localPreview; setLocalPreview(newVal); writeLocalPreview(path, newVal); @@ -96,92 +98,100 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block const version = meta?.version ?? defaultVersion; const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview); + const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog; return ( -
- {/* Version badge */} - - Версия: - - {editing ? ( - - setVersionInput(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} - autoFocus - /> - + <> +
+ {/* Version badge */} + + Версия: + + {editing ? ( + + setVersionInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} + autoFocus + /> + + + + ) : ( - - ) : ( + )} + + · + + {/* Preview toggle */} - )} - - · - - {/* Preview toggle */} - - - {apiDown && ( - <> - · - API офлайн - - )} -
+ + {/* Subtle offline dot instead of "API офлайн" text */} + {apiDown && ( + + )} +
+ + {/* Changelog rendered from API or fallback */} + {changelog.length > 0 && } + ); } diff --git a/docs/SPRINTS.md b/docs/SPRINTS.md index f22114d..786558f 100644 --- a/docs/SPRINTS.md +++ b/docs/SPRINTS.md @@ -239,9 +239,17 @@ - Фон новостей: #f2fee6 (ранее #fff) - Типографика сайта: обновлена таблица стилей - [x] FE: Добавлена «История версий» с changelog на каждую страницу блока +- [x] BE: Метаданные блоков (version, isInPreview, changelog) перенесены в PostgreSQL + - Prisma schema: добавлено поле `changelog Json @default("[]")` + - Seed обновлён: 8 блоков с актуальными версиями и историей изменений + - API: PATCH /blocks/by-path принимает changelog + - CORS: открыт для любого localhost порта + - BlockMetaBar: загружает version + changelog из API, fallback на defaultVersion/defaultChangelog + - Компонент BlockChangelog: отображает историю версий из API или из кода + - Надпись «API офлайн» заменена на серую точку - [ ] FE: Footer -**Результат спринта:** Hero v1.2, Doctors v1.2, Reviews v1.1, ContactForms v1.1, News v1.1 — все стили синхронизированы с реальным сайтом. +**Результат спринта:** Hero v1.2, Doctors v1.2, Reviews v1.1, ContactForms v1.1, News v1.1 — стили синхронизированы с реальным сайтом. Метаданные блоков хранятся в БД. --- @@ -285,14 +293,17 @@ ### Задачи -- [ ] FE: Страница `/pages/preview` — пустое состояние с кнопкой «Создать» -- [ ] FE: Логика `localStorage` — сохранение/сброс состояния превью -- [ ] FE: Рефактор `/blocks/hero/page.tsx` — вынести баннер в компонент `HeroBlock` (переиспользуемый) -- [ ] FE: Рефактор `/blocks/doctors/page.tsx` — вынести в компонент `DoctorsBlock` -- [ ] FE: Placeholder-компонент для блоков, которые ещё не готовы (серая рамка с названием блока) -- [ ] FE: Сборка превью: рендер всех доступных компонентов в порядке реального сайта -- [ ] FE: Sidebar — добавить «Просмотр страницы» в раздел «Страницы» -- [ ] FE: Кнопка «Пересобрать» в созданном состоянии +- [x] FE: Страница `/pages/preview` — пустое состояние с кнопкой «Создать» +- [x] FE: Логика `localStorage` — сохранение/сброс состояния превью +- [x] FE: Рефактор `/blocks/hero/page.tsx` — вынести баннер в компонент `HeroBlock` (переиспользуемый) +- [x] FE: Рефактор `/blocks/doctors/page.tsx` — вынести в компонент `DoctorsBlock` +- [x] FE: Placeholder-компонент для блоков, которые ещё не готовы (серая рамка с названием блока) +- [x] FE: Сборка превью: рендер всех доступных компонентов в порядке реального сайта +- [x] FE: Sidebar — добавить «Просмотр страницы» в раздел «Страницы» +- [x] FE: Кнопка «Пересобрать» в созданном состоянии +- [x] FE: Toggle «В превью» через localStorage (fallback при API офлайн) +- [x] BE: BlockMetaBar + PreviewClient подключены к NestJS API `/blocks` +- [x] BE: Метаданные блоков (version, changelog, isInPreview) в PostgreSQL - [ ] Docs: Добавить `/pages/preview` v1.0 в LLM_CONTEXT.md ### Зависимости