diff --git a/docs/design/architecture-map.html b/docs/design/architecture-map.html index 92f577b..ec3e9e4 100644 --- a/docs/design/architecture-map.html +++ b/docs/design/architecture-map.html @@ -53,11 +53,9 @@ outline: 3px solid #d79078; outline-offset: 2px; } - .legend { display: flex; flex-wrap: wrap; gap: 15px; color: var(--muted); } + .legend { display: flex; flex-wrap: wrap; gap: 8px 20px; color: var(--muted); } .legend-item { display: inline-flex; align-items: center; gap: 7px; } - .legend-line { width: 34px; border-top: 2px solid var(--line); } - .legend-line.dashed { border-top-style: dashed; } - .legend-line.dotted { border-top-style: dotted; } + .legend-icon { display: block; width: 50px; height: 20px; flex: none; } main { display: grid; grid-template-columns: minmax(0, 1fr) 370px; @@ -71,7 +69,7 @@ border-radius: 10px; background: var(--panel); } - svg { display: block; width: 100%; min-width: 1040px; height: auto; } + #map { display: block; width: 100%; min-width: 1040px; height: auto; } .layer-title { font-size: 15px; font-weight: 700; text-anchor: middle; } .layer-caption { font-size: 11px; fill: var(--muted); text-anchor: middle; } .zone-caption { font-size: 12px; fill: var(--muted); } @@ -87,7 +85,7 @@ .edge-line.dotted { stroke-dasharray: 2 6; stroke-linecap: round; } .edge-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; } .node { cursor: pointer; transition: opacity .15s; } - .node rect { + .node rect, .legend-node rect { fill: #f1f2f2; stroke: #858b90; stroke-width: 1.2; @@ -130,12 +128,12 @@ a { color: #8a351e; text-underline-offset: 2px; } .hint { color: var(--muted); font-size: 13px; } .edge[hidden] { display: none; } - .node.student rect { stroke-dasharray: 5 3; } + .node.student rect, .legend-node.student rect { stroke-dasharray: 5 3; } .node.student .stream-mark { fill: #875a14; } - .edge.expected .edge-line { stroke-dasharray: 5 4; } - .node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; } - .node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; } - .node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); } + .edge.expected .edge-line, .legend-arrow.expected .edge-line { stroke-dasharray: 5 4; } + .node.is-upstream rect, .legend-node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; } + .node.is-downstream rect, .legend-node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; } + .node.is-selected rect, .legend-node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); } .relation-button { border-radius: 5px; padding: 5px 7px; text-align: left; width: 100%; margin-top: 5px; font-size: 13px; } .badge { font-size: 13px; padding: 3px 8px; border-radius: 4px; background: #eef1ed; display: inline-block; } .badge.student { background: #fff1cf; color: #775018; } @@ -155,9 +153,41 @@

-
- Сплошная рамка: готовоПунктирная рамка: задание - Синий: источникиЗеленый: потребителиОранжевый: выбор +
+ + + Готовая загрузка + + + + Загрузка по ТЗ + + + + Выделенные связи + +
+
+ + + Готовая таблица + + + + Учебная заглушка + + + + Источники + + + + Потребители + + + + Выбранная таблица +
@@ -5142,9 +5172,11 @@ } function renderState(message = "") { document.querySelectorAll("[data-view]").forEach(b => b.setAttribute("aria-pressed",String(b.dataset.view === state.view))); - document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Синий: источники" : "Синий: связанные родители"; - document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Зеленый: потребители" : "Зеленый: связанные потомки"; - document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Пунктир — ожидается по ТЗ. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK."; + document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Источники" : "Связанные родители"; + document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Потребители" : "Связанные потомки"; + document.getElementById("ready-edge-legend").textContent = state.view === "flow" ? "Готовая загрузка" : "Связь по ключам"; + document.getElementById("expected-edge-legend").textContent = state.view === "flow" ? "Загрузка по ТЗ" : "Связь с учебной заглушкой"; + document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK."; if (state.selected?.type === "node") renderNodeCard(nodeById.get(state.selected.id)); else if (state.selected?.type === "edge") renderEdgeCard(edgeById.get(state.selected.id)); else renderIntro(message); diff --git a/docs/design/architecture-map/template.html b/docs/design/architecture-map/template.html index 60cede3..6baf267 100644 --- a/docs/design/architecture-map/template.html +++ b/docs/design/architecture-map/template.html @@ -52,11 +52,9 @@ outline: 3px solid #d79078; outline-offset: 2px; } - .legend { display: flex; flex-wrap: wrap; gap: 15px; color: var(--muted); } + .legend { display: flex; flex-wrap: wrap; gap: 8px 20px; color: var(--muted); } .legend-item { display: inline-flex; align-items: center; gap: 7px; } - .legend-line { width: 34px; border-top: 2px solid var(--line); } - .legend-line.dashed { border-top-style: dashed; } - .legend-line.dotted { border-top-style: dotted; } + .legend-icon { display: block; width: 50px; height: 20px; flex: none; } main { display: grid; grid-template-columns: minmax(0, 1fr) 370px; @@ -70,7 +68,7 @@ border-radius: 10px; background: var(--panel); } - svg { display: block; width: 100%; min-width: 1040px; height: auto; } + #map { display: block; width: 100%; min-width: 1040px; height: auto; } .layer-title { font-size: 15px; font-weight: 700; text-anchor: middle; } .layer-caption { font-size: 11px; fill: var(--muted); text-anchor: middle; } .zone-caption { font-size: 12px; fill: var(--muted); } @@ -86,7 +84,7 @@ .edge-line.dotted { stroke-dasharray: 2 6; stroke-linecap: round; } .edge-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; } .node { cursor: pointer; transition: opacity .15s; } - .node rect { + .node rect, .legend-node rect { fill: #f1f2f2; stroke: #858b90; stroke-width: 1.2; @@ -129,12 +127,12 @@ a { color: #8a351e; text-underline-offset: 2px; } .hint { color: var(--muted); font-size: 13px; } .edge[hidden] { display: none; } - .node.student rect { stroke-dasharray: 5 3; } + .node.student rect, .legend-node.student rect { stroke-dasharray: 5 3; } .node.student .stream-mark { fill: #875a14; } - .edge.expected .edge-line { stroke-dasharray: 5 4; } - .node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; } - .node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; } - .node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); } + .edge.expected .edge-line, .legend-arrow.expected .edge-line { stroke-dasharray: 5 4; } + .node.is-upstream rect, .legend-node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; } + .node.is-downstream rect, .legend-node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; } + .node.is-selected rect, .legend-node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); } .relation-button { border-radius: 5px; padding: 5px 7px; text-align: left; width: 100%; margin-top: 5px; font-size: 13px; } .badge { font-size: 13px; padding: 3px 8px; border-radius: 4px; background: #eef1ed; display: inline-block; } .badge.student { background: #fff1cf; color: #775018; } @@ -154,9 +152,41 @@

-
- Сплошная рамка: готовоПунктирная рамка: задание - Синий: источникиЗеленый: потребителиОранжевый: выбор +
+ + + Готовая загрузка + + + + Загрузка по ТЗ + + + + Выделенные связи + +
+
+ + + Готовая таблица + + + + Учебная заглушка + + + + Источники + + + + Потребители + + + + Выбранная таблица +
@@ -393,9 +423,11 @@ } function renderState(message = "") { document.querySelectorAll("[data-view]").forEach(b => b.setAttribute("aria-pressed",String(b.dataset.view === state.view))); - document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Синий: источники" : "Синий: связанные родители"; - document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Зеленый: потребители" : "Зеленый: связанные потомки"; - document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Пунктир — ожидается по ТЗ. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK."; + document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Источники" : "Связанные родители"; + document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Потребители" : "Связанные потомки"; + document.getElementById("ready-edge-legend").textContent = state.view === "flow" ? "Готовая загрузка" : "Связь по ключам"; + document.getElementById("expected-edge-legend").textContent = state.view === "flow" ? "Загрузка по ТЗ" : "Связь с учебной заглушкой"; + document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK."; if (state.selected?.type === "node") renderNodeCard(nodeById.get(state.selected.id)); else if (state.selected?.type === "edge") renderEdgeCard(edgeById.get(state.selected.id)); else renderIntro(message); diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index 7accb56..241e755 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -60,7 +60,9 @@ Docker для карты не нужны. Храните HTML рядом с ос Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон первого рейса можно ошибочно присоединить ко второму. -## Как устроен поток целиком? + + +## Путь данных от источника до витрин Оставьте билет выбранным и переключитесь на **"Поток данных"**. Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки @@ -86,7 +88,9 @@ Docker для карты не нужны. Храните HTML рядом с ос проверки календаря. Порядок выполнения смотрите по ссылке **"DAG · порядок задач"** в карточке. Поля, SQL и проверки раскрываются там же, после выбора таблицы. -## Как собирается факт? + + +## Сборка факта продаж Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales). В нашем примере в нем будет **три строки**, по одной на каждый сегмент. @@ -111,7 +115,9 @@ SK здесь обозначены словами: конкретные числ Нет талона - сегмент сохраняется, `seat_no` остается `NULL`, `is_boarded = false`. В этом стенде признак посадки означает именно наличие талона. -### Откуда берутся два аэропорта + + +### Аэропорты вылета и прилета В карточке факта откройте связи **"Коды двух аэропортов"**, **"Аэропорт вылета"** и **"Аэропорт прилета"**: @@ -128,7 +134,9 @@ SK здесь обозначены словами: конкретные числ SQL использует `LEFT JOIN`, поэтому строка с таким пропуском сохранится; допустимость пропуска отдельно оценивает [DQ факта](../../sql/dds/fact_flight_sales_dq.sql). -### Какая версия маршрута нужна рейсу + + +### Версия маршрута на дату рейса Связь **"Версия на дату рейса"** ведет к `dds.dim_routes` по `route_bk = route_no` и дате `scheduled_departure::DATE`. Дата должна попасть в полуоткрытый @@ -139,7 +147,9 @@ SQL использует `LEFT JOIN`, поэтому строка с таким поиска `airplane_sk` в `dim_airplanes`. Поэтому одного заполненного справочника моделей недостаточно: без подходящей версии маршрута `airplane_sk` не появится. -## Что кормит витрины? + + +## Агрегация фактов в витрине продаж Перейдите к [dm.sales_report](architecture-map.html#node=dm.sales_report). Зерно меняется: день вылета × два аэропорта × класс обслуживания. diff --git a/docs/design/reading_the_pipeline.md b/docs/design/reading_the_pipeline.md index 93bb085..01db211 100644 --- a/docs/design/reading_the_pipeline.md +++ b/docs/design/reading_the_pipeline.md @@ -43,8 +43,9 @@ 11. `LEFT JOIN dds.dim_passengers` даёт `passenger_sk`. 12. `LEFT JOIN ods.boarding_passes` даёт `seat_no` и признак `is_boarded`. -Так список связей превращается в [схему «Как собирается факт?»](db_schema.md#как-собирается-факт): -четыре обязательных входа рисуются сплошными стрелками, а остальные пунктирными. +Этот путь разобран в разделе [«Сборка факта продаж»](db_schema.md#сборка-факта-продаж). +На карте сплошные стрелки показывают готовые загрузки, пунктирные — ожидаемые +по ТЗ. Тип соединения и поведение при отсутствии строки описаны в карточке связи. ## Как проверить свой слой