feat(docs): /docs/b2b-channels (rendered) + /docs/b2b-channels.md (raw) for sharing
This commit is contained in:
parent
59466de811
commit
13119f9cb5
4 changed files with 282 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
84
frontend/public/docs/b2b-channels.md
Normal file
84
frontend/public/docs/b2b-channels.md
Normal 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)
|
||||
83
frontend/src/app/docs/b2b-channels/page.tsx
Normal file
83
frontend/src/app/docs/b2b-channels/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
frontend/src/app/docs/b2b-channels/renderMarkdown.ts
Normal file
103
frontend/src/app/docs/b2b-channels/renderMarkdown.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Минимальный markdown→HTML renderer без внешних зависимостей.
|
||||
* Поддерживает: # / ## / ### headings, **bold**, `code`, ссылки [text](url),
|
||||
* списки -/* / 1., чекбоксы [ ]/[x], абзацы.
|
||||
* Не поддерживает: tables, blockquotes, code fences (для нашего docs хватает).
|
||||
*/
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue