feat(sprint-5.5): store block metadata (version, changelog) in PostgreSQL

- Prisma schema: added `changelog Json @default("[]")` to Block model
- Migration: 20260324141120_add_changelog_field
- Seed: 8 blocks with actual versions (v1.0–v1.2) and changelog entries
- API: PATCH /blocks/by-path accepts changelog field
- CORS: accept any localhost port (regex pattern)
- BlockChangelog component: renders version history from API or fallback
- BlockMetaBar: loads changelog from API, passes to BlockChangelog
  - Removed "API офлайн" text, replaced with subtle gray dot
  - Added defaultChangelog prop for offline fallback
- Block pages: removed hardcoded changelog JSX, use defaultChangelog prop
- Updated SPRINTS.md with completed tasks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-03-24 19:21:56 +05:00
parent 36d5faf67d
commit e20d222183
14 changed files with 349 additions and 197 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Block" ADD COLUMN "changelog" JSONB NOT NULL DEFAULT '[]';
+1
View File
@@ -46,6 +46,7 @@ model Block {
name String
version String
isInPreview Boolean @default(false)
changelog Json @default("[]")
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
+108 -9
View File
@@ -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,
});
}
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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 });
}
}
+1 -1
View File
@@ -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();