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
+14 -16
View File
@@ -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 (
<div className="p-8 max-w-5xl mx-auto space-y-10">
@@ -64,7 +77,7 @@ export default function ContactFormsPage() {
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
Формы записи
</h1>
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.1" defaultIsInPreview={false} />
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Два блока форм с perm.oclinica.ru/lor запись на приём и запрос стоимости операции.
</p>
@@ -112,21 +125,6 @@ export default function ContactFormsPage() {
</div>
</section>
{/* Changelog */}
<section className="space-y-3">
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
История версий
</h2>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 24.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
<li>H2: размер на <strong>36px</strong>, цвет на <strong>#000000</strong>, line-height 38px</li>
<li>Фон формы 1: с #b8e6ed на <strong>#d4f6f8</strong></li>
<li>Фон формы 2: с #ffffff на <strong>#d4f6f8</strong> (обе формы на одном фоне)</li>
</ul>
</div>
</section>
{/* LLM блок */}
<LlmBlock path="/blocks/contact-forms" version="v1.1" specText={LLM_FORMS_TEXT}>
<LlmSection title="Форма 1 — Запись на приём" />
+21 -24
View File
@@ -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 (
<div className="p-8 max-w-5xl mx-auto space-y-10">
@@ -59,7 +79,7 @@ export default function DoctorsBlockPage() {
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
Блок «Наши врачи»
</h1>
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} />
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Блок на странице perm.oclinica.ru/lor заголовок, 3 стат-блока, сетка из 6 карточек врачей.
</p>
@@ -101,29 +121,6 @@ export default function DoctorsBlockPage() {
</div>
</section>
{/* Changelog */}
<section className="space-y-3">
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
История версий
</h2>
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.2 24.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li>H2: размер с ~30px на <strong>36px</strong>, цвет с #111827 на <strong>#000000</strong></li>
<li>H2 line-height: <strong>38px</strong></li>
</ul>
</div>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 23.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li>6 реальных фото врачей с сайта</li>
<li>Статистика без фона, только border-bottom #60959c</li>
</ul>
</div>
</div>
</section>
{/* LLM блок */}
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}>
<LlmSection title="Структура блока" />
+23 -26
View File
@@ -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 (
<div className="p-8 max-w-5xl mx-auto space-y-10">
@@ -64,7 +86,7 @@ export default function HeroPage() {
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
Hero-баннер
</h1>
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.2" defaultIsInPreview={true} />
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Главный баннер страницы раздела ЛОР perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
<strong>#f9f4e7</strong>.
@@ -138,31 +160,6 @@ export default function HeroPage() {
</p>
</section>
{/* Changelog */}
<section className="space-y-3">
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
История версий
</h2>
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.2 24.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li>H1: цвет исправлен с #53514e на <strong>#cb9768</strong>, размер с ~20px на <strong>36px</strong></li>
<li>Заголовок баннера: размер с ~16px на <strong>22px</strong>, цвет с #111827 на <strong>#333333</strong></li>
<li>CTA-кнопка: стиль изменён с outline на <strong>pill</strong> (фон #E9E4D4, radius 25px)</li>
<li>Дефис в H1: длинное тире «» заменено на простой дефис «-» (как на сайте)</li>
</ul>
</div>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 23.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li>Единый фон #f9f4e7 (ранее был разбит на две зоны)</li>
<li>Реальное фото врача с пациентом</li>
</ul>
</div>
</div>
</section>
{/* LLM блок */}
<LlmBlock path="/blocks/hero" version="v1.2" specText={LLM_HERO_TEXT}>
<LlmSection title="Структура баннера" />
+13 -15
View File
@@ -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 (
<div className="p-8 max-w-5xl mx-auto space-y-10">
@@ -81,7 +93,7 @@ export default function NewsBlockPage() {
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
Блок «Новости»
</h1>
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} />
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Блок новостей с perm.oclinica.ru/lor 4 карточки в ряд (дата + заголовок-ссылка),
кнопка «Все новости».
@@ -132,20 +144,6 @@ export default function NewsBlockPage() {
</p>
</section>
{/* Changelog */}
<section className="space-y-3">
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
История версий
</h2>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 24.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
<li>H2: размер на <strong>36px</strong>, цвет на <strong>#000000</strong>, line-height 38px</li>
<li>Фон секции: с белого на <strong>#f2fee6</strong> (светло-зелёный, как на реальном сайте)</li>
</ul>
</div>
</section>
{/* LLM блок */}
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}>
<LlmSection title="Структура карточки новости" />
+12 -14
View File
@@ -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 (
<div className="p-8 max-w-5xl mx-auto space-y-10">
@@ -66,7 +77,7 @@ export default function ReviewsBlockPage() {
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
Блок «Отзывы о нас»
</h1>
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} />
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Карусель отзывов с perm.oclinica.ru/lor большая кавычка, текст, «Читать полностью», стрелки.
</p>
@@ -157,19 +168,6 @@ export default function ReviewsBlockPage() {
</div>
</section>
{/* Changelog */}
<section className="space-y-3">
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
История версий
</h2>
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 24.03.2026</p>
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
<li>H2: размер с ~20px на <strong>36px</strong>, цвет с #111827 на <strong>#000000</strong>, line-height 38px</li>
</ul>
</div>
</section>
{/* LLM блок */}
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}>
<LlmSection title="Структура блока" />