feat(docs): /docs/b2b-channels (rendered) + /docs/b2b-channels.md (raw) for sharing

This commit is contained in:
lekss361 2026-05-11 23:36:02 +03:00
parent 59466de811
commit 13119f9cb5
4 changed files with 282 additions and 0 deletions

View file

@ -33,6 +33,18 @@ const nextConfig: NextConfig = {
}, },
]; ];
}, },
// Markdown в public/docs/ — отдаём с text/markdown + charset, чтобы
// браузер показывал inline (не download).
async headers() {
return [
{
source: "/docs/:path*.md",
headers: [
{ key: "Content-Type", value: "text/markdown; charset=utf-8" },
],
},
];
},
}; };
export default nextConfig; export default nextConfig;

View file

@ -0,0 +1,84 @@
# B2B-каналы для site-finder данных
**Эшелон:** 🤝 B2B / коммерческий канал — для Макса
## Контекст / проблема
Текущий scoring строится на open-data (OSM, ДОМ.РФ public API, ФГИС ТП HTML, ДДУ из открытых выгрузок). Это покрывает ~70% сигналов, но **30% решающих факторов для МКД требуют B2B-договорённостей** с разными контрагентами.
Этот документ — **реестр всех известных B2B-вопросов**, которые надо отдать Максу для проработки/договорённостей. По мере появления — комментарии.
## Реестр B2B-направлений
### 1. Инженерные сети (РСО) — высокий приоритет
- **МРСК Урала (электроэнергия):** доступная мощность по подстанции, прогноз очерёдности подключения, тариф за кВт мощности
- Вопрос: есть ли API/выгрузка по нагрузке per substation? Иначе нужен подписанный договор обмена данными.
- **МУП «Водоканал» Екатеринбурга:** наличие магистральных сетей в radius 200м, мощность, точки подключения
- Вопрос: предоставляют ли GIS-слой публичных сетей? Условия?
- **Газпром межрегионгаз / ГРО** — газоснабжение
- **СТК (Свердловская теплоснабжающая компания) / Т Плюс** — теплосеть, доступная нагрузка
**Альтернатива:** покупка от одного агрегатора (есть провайдеры типа ТехУслуги/Атон) — узнать у Макса контакты.
### 2. ЗОУИТ из ФГИС ТП — high priority для G3
- **Минэкономразвития РФ / Росреестр** — ФГИС ТП (fgistp.economy.gov.ru) даёт shape ЗОУИТ, но через личный кабинет / HTML-парсинг. Возможен ли B2B-доступ к структурированным выгрузкам?
- **Прокадастр / EGRN.reestr.net** — платные API ~10-30K ₽/мес выдают декодированные ЗОУИТ. Узнать у Макса какой провайдер удобнее.
### 3. ПЗЗ — medium priority для G1
- **Минстрой Свердл / Администрация ЕКБ** — официальный shape ПЗЗ с параметрами. data.midural.ru может не отдавать полноту.
- Узнать у Макса контакт в Минстрое.
### 4. Шахматки застройщиков — high priority для data moat
- **Топ-7 застройщиков Екб** (Атомстрой, Брусника, ТЭН, Стройтек, Прогресс, УГМК, КОРТРОС) — публичные шахматки на сайтах
- Web-scrape технически возможен, но **юр-партнёрство снижает риск исков**
- Из SalesTracking_Strategy_Apr25: «партнёрство возамен аналитики» с топ-3
- Узнать у Макса, кто открыт к диалогу
### 5. Аналитика продаж конкурентов — medium priority для D5
- **Объектив API** — уже используем (Антон), но ограниченный план
- Узнать про upgrade тариф и full Свердл coverage
- **Сбер ДДФ** — есть `sber_ddf_metrics` в БД, но не полно
- Узнать про подписку
### 6. Демография — medium priority для I1
- **Росстат** — публично, но per-район ЕКБ грануляр­ность OKATO/OKTMO
- Альтернатива: купить срез у Финама/Спарка/etc
### 7. CRM данных других девелоперов — long-term для D5
- Сейчас только PRINZIP (через Антона)
- B2B-предложение «вы даёте свой CRM, мы аналитику соседей» — нужны 2-3 договорённости для значимой агрегации
### 8. ОКН (объекты культурного наследия)
- Свердл реестр не структурирован, нужен HTML-парсинг
- Узнать про официальный shape от Управления ОКН Свердл
## Что от Макса
- ✅ Подтвердить приоритеты в реестре (что критично сейчас vs «можно подождать»)
- ✅ Контакты в РСО, Минстрое, Минэкономразвития
- ✅ Бюджет на платные API (Прокадастр ≈30K ₽/мес, Объектив upgrade — узнать прайс)
- ✅ Аналог LOI или MOU с топ-3 застройщиками Екб
- ✅ Кто готов давать данные за аналитику (partnership offer)
## Acceptance
- [ ] Этот документ — **постоянный реестр** (живёт до Раунда А)
- [ ] Каждый new B2B-вопрос → добавление в раздел с тегом контрагента
- [ ] Раз в 2 недели — sync с Максом, статусы переводятся (📞 contacted / ✅ agreed / ❌ declined)
## Owner
- **Coordinator:** main session ↔ Макс
- Реализация после получения каналов: соответствующий worker-агент (database-expert для shapefile import, backend-engineer для API integration)
## Effort
multi-week (не technical, organizational)

View file

@ -0,0 +1,83 @@
import fs from "node:fs";
import path from "node:path";
import Link from "next/link";
import { renderMarkdown } from "./renderMarkdown";
export const metadata = {
title: "B2B-каналы для site-finder · GenDesign",
description: "Реестр B2B-вопросов для координации с Максом",
};
export default function B2BChannelsPage() {
const mdPath = path.join(process.cwd(), "public", "docs", "b2b-channels.md");
const raw = fs.readFileSync(mdPath, "utf-8");
return (
<main
style={{
maxWidth: 880,
margin: "0 auto",
padding: "32px 24px",
fontFamily:
'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
color: "#1f2937",
lineHeight: 1.6,
}}
>
<div
style={{
marginBottom: 24,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
paddingBottom: 16,
borderBottom: "1px solid #e5e7eb",
}}
>
<Link
href="/"
style={{ fontSize: 13, color: "#6b7280", textDecoration: "none" }}
>
Главная
</Link>
<a
href="/docs/b2b-channels.md"
download
style={{
fontSize: 13,
color: "#1d4ed8",
textDecoration: "none",
padding: "6px 12px",
border: "1px solid #1d4ed8",
borderRadius: 6,
}}
>
Скачать .md
</a>
</div>
<div
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: renderMarkdown(raw) }}
className="markdown-body"
/>
<style
dangerouslySetInnerHTML={{
__html: `
.markdown-body h1 { font-size: 28px; font-weight: 700; margin: 24px 0 16px; line-height: 1.3; }
.markdown-body h2 { font-size: 22px; font-weight: 700; margin: 32px 0 12px; padding-bottom: 6px; border-bottom: 1px solid #e5e7eb; }
.markdown-body h3 { font-size: 17px; font-weight: 600; margin: 24px 0 8px; color: #1d4ed8; }
.markdown-body p { margin: 8px 0; }
.markdown-body ul { margin: 8px 0; padding-left: 24px; }
.markdown-body li { margin: 4px 0; }
.markdown-body code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-family: ui-monospace, monospace; font-size: 0.92em; color: #b91c1c; }
.markdown-body strong { font-weight: 600; }
.markdown-body a { color: #1d4ed8; text-decoration: underline; }
`,
}}
/>
</main>
);
}

View file

@ -0,0 +1,103 @@
/**
* Минимальный markdownHTML renderer без внешних зависимостей.
* Поддерживает: # / ## / ### headings, **bold**, `code`, ссылки [text](url),
* списки -/* / 1., чекбоксы [ ]/[x], абзацы.
* Не поддерживает: tables, blockquotes, code fences (для нашего docs хватает).
*/
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function inlineMd(line: string): string {
// escape first, then re-introduce safe markup
let html = escapeHtml(line);
// code spans `...`
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
// bold **...**
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
// links [text](url)
html = html.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
);
// checkboxes
html = html.replace(
/\[ \]/g,
'<input type="checkbox" disabled style="margin-right:6px;"/>',
);
html = html.replace(
/\[x\]/gi,
'<input type="checkbox" checked disabled style="margin-right:6px;"/>',
);
return html;
}
export function renderMarkdown(md: string): string {
const lines = md.split("\n");
const out: string[] = [];
let inList = false;
for (const rawLine of lines) {
const line = rawLine.replace(/\r$/, "");
if (line.trim() === "") {
if (inList) {
out.push("</ul>");
inList = false;
}
continue;
}
// Headings
if (line.startsWith("### ")) {
if (inList) {
out.push("</ul>");
inList = false;
}
out.push(`<h3>${inlineMd(line.slice(4))}</h3>`);
continue;
}
if (line.startsWith("## ")) {
if (inList) {
out.push("</ul>");
inList = false;
}
out.push(`<h2>${inlineMd(line.slice(3))}</h2>`);
continue;
}
if (line.startsWith("# ")) {
if (inList) {
out.push("</ul>");
inList = false;
}
out.push(`<h1>${inlineMd(line.slice(2))}</h1>`);
continue;
}
// List items (- or * at indent 0 or 2)
const listMatch = line.match(/^(\s*)[-*]\s+(.*)$/);
if (listMatch) {
if (!inList) {
out.push("<ul>");
inList = true;
}
out.push(`<li>${inlineMd(listMatch[2])}</li>`);
continue;
}
// Plain paragraph
if (inList) {
out.push("</ul>");
inList = false;
}
out.push(`<p>${inlineMd(line)}</p>`);
}
if (inList) out.push("</ul>");
return out.join("\n");
}