В модуле вы считали точность, смещение и FVA. Теперь — как показать всё это команде в одном файле, который открывается двойным кликом, работает без интернета, переключается между русским и английским и выгружает таблицы в Excel. Так устроены наши рабочие дашборды. В этом уроке разберём, из чего такой дашборд собирается, посмотрим большой образец «Долины напитков» и скачаем маленький стартовый набор, который соберёте сами в уроке 05-19.
In this module you calculated accuracy, bias and FVA. Now — how to show it all to the team in one file that opens with a double click, works offline, switches between Russian and English and exports tables to Excel. That is how our working dashboards are built. In this lesson we take apart what such a dashboard is made of, look at the large Valley Beverages sample and download a small starter kit that you will build yourself in lesson 05-19.
Дашборд готовится как блюдо. Есть продукты — выгрузки продаж и прогноза. Есть рецепт — шаблон с вёрсткой и формулами. Повар по рецепту собирает блюдо — скрипт сборки. Перед подачей шеф пробует — скрипт проверки. И официант несёт гостям — публикация. Если пропустить дегустацию, гость первым узнает, что суп пересолен.
A dashboard is cooked like a dish. There are ingredients — sales and forecast exports. There is a recipe — a template with the layout and formulas. The cook follows the recipe — the build script. Before serving the chef tastes it — the check script. And the waiter brings it to the guests — publishing. Skip the tasting and the guest is the first to find out the soup is too salty.
| Свой HTML-файлOwn HTML file | Power BI (05-14) | Excel | |
|---|---|---|---|
| ОткрытьOpen | двойной клик, без интернетаdouble click, offline | сервис и лицензия у каждого зрителяa service and a licence per viewer | нужен Excelneeds Excel |
| Что угодно на экранеAnything on screen | да: свои листы, слайды, «откуда цифра»yes: own sheets, slides, "where does this number come from" | в рамках визуаловwithin the visuals | в рамках формул и сводныхwithin formulas and pivots |
| ОбновлениеRefresh | пересборка скриптом (05-21)a scripted rebuild (05-21) | по расписаниюscheduled | руками или Power Queryby hand or Power Query |
| Кто поддерживаетWho maintains it | вы с Claude Codeyou with Claude Code | BI-команда или выa BI team or you | выyou |
| Главный рискMain risk | данные внутри файла — его нельзя «отозвать»the data sits inside the file — it cannot be "recalled" | ловушка уровня расчёта в DAXthe calculation-grain trap in DAX | ручные ошибки и версииmanual errors and versions |
Правило выбора простое: если компания уже живёт в Power BI — стройте там. Свой файл хорош, когда нужно быстро, без лицензий и с тем, чего нет в стандартных визуалах: слайды для встречи, правки прогноза прямо в дашборде, объяснение каждой цифры.
The choice rule is simple: if the company already lives in Power BI — build there. Your own file is good when you need it fast, without licences and with things standard visuals lack: slides for the meeting, forecast edits right in the dashboard, an explanation of every number.
| ШагStep | Файл в стартовом набореFile in the starter kit | Что на входе → что на выходеInput → output |
|---|---|---|
| 1. Данные1. Data | make_data.py | выгрузки (в наборе — учебный генератор) → data.jsonexports (a training generator in the kit) → data.json |
| 2. Шаблон2. Template | template.html | вёрстка, фильтры, карточки, графики, расчёт точности; метка /*DATA*/layout, filters, cards, charts, accuracy maths; the /*DATA*/ marker |
| 3. Сборка3. Build | build_dashboard.py | шаблон + data.json → mini_dashboard.html; по пути ищет повторные объявленияtemplate + data.json → mini_dashboard.html; checks for duplicate declarations on the way |
| 4. Проверка4. Check | check_dashboard.py | дашборд в браузере без окна → ошибки JS и сверка точности с Pythondashboard in a windowless browser → JS errors and accuracy reconciled with Python |
| 5. Публикация5. Publish | run_dashboard.bat (и урок 05-21)(and lesson 05-21) | готовый файл → командеthe finished file → the team |
В рабочем дашборде шаг 1 читает настоящие выгрузки: продажи, архив версий прогноза, справочники. Как подключать свои файлы — урок 05-20. Здесь — учебный генератор, чтобы всё запускалось на любом компьютере.
In a working dashboard step 1 reads real exports: sales, the forecast version archive, master data. How to connect your own files — lesson 05-20. Here we use a training generator so everything runs on any computer.
Нажмите на шаг, чтобы выключить или включить его. Под цепочкой — что увидит команда. Выберите и ситуацию: «первый запуск» или «правка после замечания».
Click a step to switch it off or on. Below the chain — what the team will see. Pick the situation too: "first run" or "an edit after a comment".
Большой образец повторяет устройство наших рабочих дашбордов к встрече Demand Review, только на учебных данных: 3 страны, 10 городов, 15 товаров, 4 канала, факт с января 2025 по сентябрь 2026, план AOP, консенсус-прогнозы циклов 1–10 и статистические прогнозы. Один файл 2,25 МБ, библиотеки для Excel, картинок и PowerPoint — внутри.
The large sample mirrors the structure of our working dashboards for the Demand Review meeting, on training data: 3 countries, 10 cities, 15 products, 4 channels, actuals January 2025 to September 2026, the AOP plan, consensus forecasts of cycles 1–10 and statistical forecasts. One 2.25 MB file, with the Excel, image and PowerPoint libraries inside.
| ЛистSheet | Что показываетWhat it shows | Урок по темеRelated lesson |
|---|---|---|
| ОбзорOverview | факт, прогноз и план по странам одной страницейactuals, forecast and plan by country on one page | 06-07 |
| Страна 1 / 2 / 3Country 1 / 2 / 3 | города и товары страны, деревья с раскрытиемthe country's cities and products, expandable trees | 05-10 |
| КомментарииComments | причины отклонений рядом с цифрамиreasons for deviations next to the numbers | 05-11 |
| КаналыChannels | Traditional / Modern Trade, HoReCa, E-commerceTraditional / Modern Trade, HoReCa, E-commerce | 02-04 |
| АналитикаAnalytics | динамика и структура портфеляtrends and portfolio structure | 05-17 |
| Изменения прогнозаForecast changes | что и насколько изменилось против прошлого циклаwhat changed versus the previous cycle and by how much | 05-04 |
| YTG | остаток года: факт + прогноз против планаyear to go: actuals + forecast versus plan | 06-07 |
| Текущий месяцMonth to date | как закроем месяц по темпу днейhow the month will close at the daily pace | 06-09 |
| Точность (FA)Accuracy (FA) | точность по лагу, уровню и методике, склейка кодовaccuracy by lag, level and method, code glue | 05-06, 05-10 |
| Сводка DRMDRM summary | риски, возможности и решенияrisks, opportunities and decisions | 06-01 |
| Building blocks | из чего складывается рост: цена, точки, новинки, промоwhat growth is made of: price, outlets, launches, promos | 06-08 |
| Колода DRMDRM deck | 12 слайдов с заголовком-выводом, выгрузка в .pptx12 slides with takeaway headlines, export to .pptx | 05-05 |
Слева — фильтры: страны, города, каналы, формат упаковки, класс ABC/XYZ, категория. Сверху — выбор цикла. Каждый лист обязан реагировать на каждый фильтр: это одна из проверок, которые запускает скрипт образца.
On the left — filters: countries, cities, channels, pack format, ABC/XYZ class, category. At the top — the cycle selector. Every sheet must react to every filter: that is one of the checks the sample's script runs.
В учебные данные специально заложены те же ловушки, что встречаются в жизни. Найдите каждую в дашборде — это и есть задание раздела.
The training data deliberately contains the same traps you meet in real life. Find each one in the dashboard — that is this section's task.
| ЛовушкаTrap | Где искатьWhere to look | Что видноWhat you see |
|---|---|---|
| Апсайз 1,5 → 1,75 л (новый код с апреля 2026)Upsize 1.5 → 1.75 L (a new code from April 2026) | Точность (FA), кнопка склейки кодовAccuracy (FA), the code-glue button | без склейки точность «товар × страна», лаг 2, падает с 89,0 до 86,3 %without glue "product × country" accuracy at lag 2 drops from 89.0 to 86.3% |
| Делист апельсиновой газировки 0,5 л в Стране 3 с июля 2026Delisting of the 0.5 L orange soda in Country 3 from July 2026 | Точность, Страна 3Accuracy, Country 3 | код исключён из точности страны целикомthe code is excluded from the country's accuracy entirely |
| Новинка — холодный чай с персиком 1 л с июля 2025A launch — peach iced tea 1 L from July 2025 | Аналитика, Изменения прогнозаAnalytics, Forecast changes | истории меньше года — «как год назад» не работаетless than a year of history — "same as last year" does not work |
| Дефицит (кола 1,5 л в одном городе, июль)A shortage (cola 1.5 L in one city, July) | Страна, Текущий месяцCountry, Month to date | провал продаж — не провал спроса (02-07)a sales dip is not a demand dip (02-07) |
| Разные характеры версийDifferent version temperaments | YTG, Изменения прогнозаYTG, Forecast changes | консенсус оптимистичен (+5 %), стат. прогноз осторожен (−2 %), план амбициозен (+8 %)consensus is optimistic (+5%), the statistical forecast cautious (−2%), the plan ambitious (+8%) |
Сверено скриптом проверки образца 30.10.2026: 56 отрисовок листов без ошибок, точность «город × товар», лаг 1 — 80 % при BIAS −2,0 %; «товар × страна», лаг 2 — 89,0 %; «суббренд × страна», лаг 4 — 88,6 %.
Reconciled by the sample's check script on 30.10.2026: 56 sheet renders without errors, "city × product" accuracy at lag 1 — 80% with BIAS −2.0%; "product × country" at lag 2 — 89.0%; "sub-brand × country" at lag 4 — 88.6%.
Образец — чтобы посмотреть и покрутить. Стартовый набор — чтобы собрать свой: 6 текстовых файлов, мини-дашборд «3 города × 4 товара × 21 месяц» с фильтрами, карточками, малыми графиками, таблицей, кнопкой «Откуда цифра?» и выгрузкой в CSV. Дополнительные библиотеки Python не нужны. Сохраните все файлы в одну папку.
The sample — to look at and play with. The starter kit — to build your own: 6 text files, a mini dashboard "3 cities × 4 products × 21 months" with filters, cards, small charts, a table, a "Where does this number come from?" button and a CSV export. No extra Python libraries needed. Save all files into one folder.
Тексты файлов можно открыть прямо здесь и скопировать — в последней части урока.
You can open the file texts right here and copy them — in the last part of the lesson.
Важно: Claude не сидит внутри дашборда. Готовый файл — обычная страница, он работает без Claude, без интернета и без ключей. Подробно, как ставить задачу ассистенту, — урок 04-28-3, как работать с ним над данными — 02-08.
Important: Claude does not live inside the dashboard. The finished file is an ordinary page that works without Claude, without internet and without keys. How to brief the assistant — lesson 04-28-3; working with it on data — 02-08.
| ПравилоRule | ПочемуWhy |
|---|---|
| 1. После любой правки — пересборка1. Rebuild after every edit | проверка data.json не заменяет пересборку: экран показывает то, что вшито в HTMLchecking data.json does not replace a rebuild: the screen shows what is embedded in the HTML |
| 2. Одна единица — тысячи2. One unit — thousands | в миллионах теряется хвост, важный на уровне товара; длину числа решают шрифтомmillions lose the tail that matters at product level; long numbers are handled by font size |
| 3. Больше 3–4 рядов — малые графики3. More than 3–4 series — small multiples | восемь линий на одном графике не читаются; плитки с общей шкалой — читаютсяeight lines on one chart are unreadable; tiles with a shared scale are readable |
| 4. Выгрузка в Excel — числа числами4. Excel export — numbers as numbers | с разделителями разрядов, формат классом, без заливок; иначе в Excel не посчитатьwith thousand separators, format by class, no fills; otherwise Excel cannot calculate |
| 5. Имя файла несёт фильтры5. The file name carries the filters | «Точность_2026_Ветровск+Каменск_все товары.csv» — через неделю видно, что выгружали"Accuracy_2026_Vetrovsk+Kamensk_all products.csv" — a week later you still know what was exported |
| 6. Объяснялка вместо ключа API6. An explainer instead of an API key | «откуда цифра» считает сам HTML и называет источник; ключ в файле увидит любойthe HTML itself computes "where the number comes from" and names the source; anyone would see a key in the file |
| 7. Повторные объявления — до публикации7. Duplicate declarations — before publishing | две одинаковые const — пустая страница у всех; две одинаковые function — молча побеждает последняяtwo identical consts — a blank page for everyone; two identical functions — the last one silently wins |
| 8. Цифра на экране = цифра в расчёте8. On-screen number = calculated number | скрипт проверки сверяет точность с Python; без сверки не публикуемthe check script reconciles accuracy with Python; no reconciliation — no publishing |
Хочется добавить в дашборд «умного помощника», который отвечает на вопросы. Для этого нужен ключ доступа к сервису ИИ. Но HTML-файл может открыть кто угодно и посмотреть его текст — ключ утечёт вместе с файлом. Поэтому мы делаем иначе: дашборд сам собирает объяснение из тех же цифр, что на экране, и называет источник.
It is tempting to add a "smart assistant" that answers questions. That needs an access key to an AI service. But anyone can open an HTML file and read its text — the key would leak with the file. So we do it differently: the dashboard builds the explanation itself from the same numbers on screen and names the source.
| РукамиBy hand | Как называетсяWhat it is called |
|---|---|
| скачали выгрузки, свели в одну таблицуdownloaded the exports, merged them into one table | ETL — извлечь, преобразовать, загрузитьETL — extract, transform, load |
| один файл, который открывается без сервераone file that opens without a server | автономное одностраничное приложениеa self-contained single-page app |
| открыли дашборд и посмотрели, всё ли на местеopened the dashboard and looked whether everything is there | дымовой тест (smoke test) в браузере без окна (headless)a smoke test in a headless browser |
| сверили цифру на экране с Excelreconciled the on-screen number with Excel | сверка (reconciliation), регрессионная проверкаreconciliation, a regression check |
Визуальная проверка обязательна. Скрипт ловит ошибки кода и расхождения цифр, но не видит, что график «не про то», подпись обрезана, а в английской версии остался русский текст. Перед публикацией откройте дашборд сами: пройдите все листы, переключите язык, поставьте два-три фильтра, сверьте одну цифру с эталоном.
Visual check is mandatory. The script catches code errors and number mismatches, but cannot see that a chart is "about the wrong thing", a label is cut off, or Russian text is left in the English version. Before publishing open the dashboard yourself: go through every sheet, switch the language, set two or three filters, reconcile one number with a reference.
Для Windows есть собственный установщик одной командой, без прав администратора, с автообновлением, и отдельное приложение для компьютера. Как ставить — урок 05-19.
Windows has a native one-command installer, no admin rights needed, with automatic updates, and a separate desktop app. How to install — lesson 05-19.
29.09.2025 Microsoft объявил режим агента в Excel: он сам строит листы и формулы по запросу. Для дашборда-файла это соседний путь — проверяйте результат так же строго.
On 29.09.2025 Microsoft announced Agent Mode in Excel: it builds sheets and formulas on request. For a dashboard file it is a neighbouring route — check its output just as strictly.
Источники: документация Claude Code (code.claude.com/docs: setup, desktop-quickstart); GeekWire, 29.09.2025 — режим агента в Excel.
Sources: Claude Code documentation (code.claude.com/docs: setup, desktop-quickstart); GeekWire, 29.09.2025 — Agent Mode in Excel.
Три случая из работы планера в компании-производителе напитков. Названия изменены.
Three cases from planning work at a beverage manufacturer. Names are changed.
В книге: «выгрузка» — 18 строк «город × товар × месяц», то, что дашборд читает на входе; «итоги» — те же меры, что карточки дашборда; «словарь данных» — откуда каждый лист образца берёт цифры.
The workbook: "export" — 18 "city × product × month" rows, what the dashboard reads as input; "totals" — the same measures as the dashboard cards; "data dictionary" — where each sheet of the sample takes its numbers.
1. Точность 93,3 %, BIAS +0,6 %: 1 − Σ|П − Ф| ÷ ΣФ и (ΣП − ΣФ) ÷ ΣФ — те же формулы, что в мини-дашборде.
2. Ветровск — 94,5 %, выше общей 93,3 %: у большого города ошибки в долях меньше.
3. «Текущий месяц»: факт ÷ прошло рабочих дней × рабочих дней в месяце (урок 06-09).
4. Данные — make_data.py; шаблон — template.html; сборка — build_dashboard.py; проверка — check_dashboard.py; публикация — run_dashboard.bat (и урок 05-21); README.md описывает всё вместе.
1. Accuracy 93.3%, BIAS +0.6%: 1 − Σ|F − A| ÷ ΣA and (ΣF − ΣA) ÷ ΣA — the same formulas as in the mini dashboard.
2. Vetrovsk — 94.5%, above the total of 93.3%: the big city's errors are smaller as a share.
3. "Month to date": actuals ÷ working days passed × working days in the month (lesson 06-09).
4. Data — make_data.py; template — template.html; build — build_dashboard.py; check — check_dashboard.py; publish — run_dashboard.bat (and lesson 05-21); README.md describes it all.
Claude Code пишет код по вашему заданию. Python считает и собирает файл. Браузер показывает. Вы ставите задачу, сверяете цифры и отвечаете за то, что ушло команде.
Claude Code writes code to your spec. Python calculates and builds the file. The browser shows it. You set the task, reconcile the numbers and own what goes to the team.
Писать код — нет. Читать сообщения об ошибках, запускать команды и проверять цифры — да. Этому и посвящён урок 05-19.
Writing code — no. Reading error messages, running commands and checking numbers — yes. That is what lesson 05-19 is about.
В образец вшиты три библиотеки: Excel, картинки слайдов и PowerPoint, а данных на два года по 10 городам. Мини-набор выгружает CSV и рисует графики сам, без библиотек.
The sample embeds three libraries — Excel, slide images and PowerPoint — and two years of data for 10 cities. The mini kit exports CSV and draws charts itself, without libraries.
Да, если файл доступен на телефоне: в шаблоне есть раскладка для узкого экрана. Но проверяйте — длинные таблицы на телефоне читать трудно.
Yes, if the file is available on the phone: the template has a narrow-screen layout. But check — long tables are hard to read on a phone.
Те же тексты, что скачиваются кнопками в разделе 8. Можно скопировать вручную.
The same texts the buttons in section 8 download. You can copy them by hand.
# Мини-дашборд «Долина напитков» — стартовый набор
Учебный набор из уроков 05-18 и 05-19. Компания, города, товары и цифры выдуманы.
## Что в папке
| Файл | Что делает |
|---|---|
| make_data.py | создаёт учебные данные data.json: 3 города × 4 товара × 21 месяц, факт, прогноз (лаг 1), дефицит |
| template.html | вёрстка и код дашборда; метка /*DATA*/ — сюда сборка вставит цифры |
| build_dashboard.py | проверяет повторные объявления в коде и собирает mini_dashboard.html |
| check_dashboard.py | открывает дашборд в Edge или Chrome без окна, ищет ошибки и сверяет точность с Python |
| run_dashboard.bat | кнопка: данные → сборка → открыть дашборд |
## Как запустить
1. Положите все файлы в одну папку с коротким путём, лучше не в OneDrive (например, C:\Dashboards\mini).
2. Откройте терминал в этой папке и выполните по очереди:
python make_data.py
python build_dashboard.py
python check_dashboard.py
3. Откройте mini_dashboard.html двойным кликом. Или просто запустите run_dashboard.bat.
Дополнительные библиотеки не нужны: хватит Python 3.10+ и браузера Edge или Chrome (браузер нужен только для проверки).
## Правила
- Правите только template.html и make_data.py. После любой правки — снова build_dashboard.py и check_dashboard.py.
- Проверка JSON не заменяет пересборку: пока не пересобрали, на экране старые цифры.
- Цифру на экране сверяйте с расчётом (кнопка «Откуда цифра?» и check_dashboard.py).
- Ключей API, паролей и личных данных в HTML не кладите: файл может открыть любой.
- В .bat только латиница: русские буквы ломают cmd. # Valley Beverages mini dashboard — starter kit
Training kit from lessons 05-18 and 05-19. The company, cities, products and numbers are fictional.
## What is in the folder
| File | What it does |
|---|---|
| make_data.py | creates the training data data.json: 3 cities × 4 products × 21 months, actuals, forecast (lag 1), shortage |
| template.html | the dashboard layout and code; the /*DATA*/ marker is where the build inserts the numbers |
| build_dashboard.py | checks the code for duplicate declarations and builds mini_dashboard.html |
| check_dashboard.py | opens the dashboard in headless Edge or Chrome, looks for errors and reconciles accuracy with Python |
| run_dashboard.bat | a button: data → build → open the dashboard |
## How to run
1. Put all files into one folder with a short path, preferably not in OneDrive (for example, C:\Dashboards\mini).
2. Open a terminal in that folder and run, one by one:
python make_data.py
python build_dashboard.py
python check_dashboard.py
3. Open mini_dashboard.html with a double click. Or just run run_dashboard.bat.
No extra libraries needed: Python 3.10+ and the Edge or Chrome browser are enough (the browser is only needed for the check).
## Rules
- Edit only template.html and make_data.py. After any edit run build_dashboard.py and check_dashboard.py again.
- Checking the JSON does not replace a rebuild: until you rebuild, the screen shows the old numbers.
- Reconcile the number on screen with a calculation (the "Where does this number come from?" button and check_dashboard.py).
- Never put API keys, passwords or personal data into the HTML: anyone can open the file.
- Latin letters only in .bat files: Cyrillic breaks cmd. # -*- coding: utf-8 -*-
"""Учебные данные мини-дашборда «Долина напитков» / Training data for the Valley Beverages mini dashboard.
Что делает / What it does:
3 города x 4 товара x 21 месяц (янв 2025 - сен 2026): факт, прогноз на месяц вперёд (лаг 1)
и флаг дефицита (OOS). Пишет data.json рядом со скриптом.
3 cities x 4 products x 21 months (Jan 2025 - Sep 2026): actuals, a one-month-ahead forecast (lag 1)
and a shortage (OOS) flag. Writes data.json next to the script.
Нужен только Python 3.10+, без дополнительных библиотек / Needs Python 3.10+ only, no extra libraries.
Запуск / Run: python make_data.py
"""
import json
import random
from pathlib import Path
HERE = Path(__file__).parent
RNG = random.Random(7) # одно и то же зерно - одни и те же цифры / same seed - same numbers
CITIES = [["Ветровск", "Vetrovsk", 1.00], ["Каменск", "Kamensk", 0.70], ["Озёрск", "Ozyorsk", 0.45]]
SKUS = [["Вода 1,5 л", "Water 1.5 L", 900, 0.25],
["Сок 1 л", "Juice 1 L", 420, 0.10],
["Холодный чай 1 л", "Iced tea 1 L", 300, 0.45],
["Лимонад 0,5 л", "Lemonade 0.5 L", 260, 0.35]]
MONTHS = [f"{y}-{m:02d}" for y in (2025, 2026) for m in range(1, 13)][:21]
OOS = {("Каменск", "Сок 1 л", "2026-06"), ("Озёрск", "Холодный чай 1 л", "2025-07")}
def season(month: int, amp: float) -> float:
"""Сезон: лето выше, зима ниже / Season: summer high, winter low."""
shape = [-0.8, -0.7, -0.3, 0.1, 0.5, 0.9, 1.0, 0.9, 0.4, 0.0, -0.5, -0.7]
return 1 + amp * shape[month - 1]
def main() -> None:
rows = []
for ci, (c_ru, _c_en, c_k) in enumerate(CITIES):
for si, (s_ru, _s_en, base, amp) in enumerate(SKUS):
for mi, ym in enumerate(MONTHS):
trend = 1 + 0.004 * mi
demand = base * c_k * season(int(ym[5:]), amp) * trend * RNG.uniform(0.92, 1.08)
oos = 1 if (c_ru, s_ru, ym) in OOS else 0
fact = demand * (0.55 if oos else 1) # дефицит съел продажи / shortage ate the sales
fc = demand * RNG.uniform(0.88, 1.14) # прогноз ошибается в обе стороны / forecast errs both ways
rows.append([ci, si, mi, round(fact), round(fc), oos])
data = {"cities": [c[:2] for c in CITIES], "skus": [s[:2] for s in SKUS], "months": MONTHS,
"unit": ["тыс. бутылок", "thousand bottles"], "rows": rows}
(HERE / "data.json").write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
r26 = [r for r in rows if MONTHS[r[2]].startswith("2026")]
fa = sum(r[3] for r in r26)
acc = 1 - sum(abs(r[4] - r[3]) for r in r26) / fa
print(f"data.json: {len(rows)} строк / rows; факт 2026 / actuals 2026 = {fa}; точность / accuracy = {acc:.4f}")
if __name__ == "__main__":
main() <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mini dashboard</title>
<style>
body{margin:0;font-family:Segoe UI,Arial,sans-serif;color:#1b2433;background:#f5f7fb}
header{display:flex;gap:12px;align-items:center;padding:12px 18px;background:#0E2B58;color:#fff}
header h1{font-size:18px;margin:0;flex:1}
header button,header select{font:inherit;border:0;border-radius:6px;padding:5px 10px;cursor:pointer}
main{display:grid;grid-template-columns:200px 1fr;gap:16px;padding:16px}
aside{background:#fff;border-radius:10px;padding:12px;font-size:14px}
aside b{display:block;margin:8px 0 4px}
aside label{display:block;margin:2px 0}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.kpi{background:#fff;border-radius:10px;padding:10px 12px}
.kpi span{font-size:12px;color:#5d6678}.kpi div{font-size:22px;font-weight:700}
.card{background:#fff;border-radius:10px;padding:12px;margin-top:12px}
.sm{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.sm h3{font-size:13px;margin:0 0 4px}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{padding:4px 8px;border-bottom:1px solid #e3e7ef;text-align:right}
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2){text-align:left}
.bad{color:#c2410c;font-weight:700}
.note{font-size:12px;color:#5d6678}
button.act{background:#2F7AD6;color:#fff;border:0;border-radius:6px;padding:6px 12px;cursor:pointer;margin-right:6px}
@media (max-width:760px){main{grid-template-columns:1fr}.kpis,.sm{grid-template-columns:1fr 1fr}}
</style>
</head>
<body>
<header><h1 id="ttl"></h1><select id="year"><option>2026</option><option>2025</option></select><button id="lang">EN</button></header>
<main>
<aside id="side"></aside>
<section>
<div class="kpis" id="kpis"></div>
<div class="card"><button class="act" id="why"></button><button class="act" id="csv"></button><div id="explain" class="note"></div></div>
<div class="card"><div class="sm" id="sm"></div></div>
<div class="card"><table id="tbl"></table></div>
</section>
</main>
<script>
const DATA=/*DATA*/;
let LANG='ru';
const ST={year:'2026',city:new Set(DATA.cities.map(function(c,i){return i})),sku:new Set(DATA.skus.map(function(s,i){return i}))};
function T(ru,en){return LANG==='ru'?ru:en}
function N(v){return Math.round(v).toLocaleString(LANG==='ru'?'ru-RU':'en-US')}
function P(v){return (v*100).toFixed(1).replace('.',LANG==='ru'?',':'.')+' %'}
function nm(pair){return pair[LANG==='ru'?0:1]}
function rows(){return DATA.rows.filter(function(r){return ST.city.has(r[0])&&ST.sku.has(r[1])&&DATA.months[r[2]].slice(0,4)===ST.year})}
function calc(rs){
var fa=0,fc=0,ae=0;
rs.forEach(function(r){fa+=r[3];fc+=r[4];ae+=Math.abs(r[4]-r[3])});
return {fact:fa,fc:fc,abs:ae,acc:fa?Math.max(0,1-ae/fa):0,bias:fa?(fc-fa)/fa:0};
}
function side(){
var h='<b>'+T('Города','Cities')+'</b>';
DATA.cities.forEach(function(c,i){h+='<label><input type="checkbox" data-c="'+i+'"'+(ST.city.has(i)?' checked':'')+'> '+nm(c)+'</label>'});
h+='<b>'+T('Товары','Products')+'</b>';
DATA.skus.forEach(function(s,i){h+='<label><input type="checkbox" data-s="'+i+'"'+(ST.sku.has(i)?' checked':'')+'> '+nm(s)+'</label>'});
document.getElementById('side').innerHTML=h;
}
function chart(si){
var ms=DATA.months.map(function(m,i){return i}).filter(function(i){return DATA.months[i].slice(0,4)===ST.year});
var fa=[],fc=[],oos=[];
ms.forEach(function(i){var rs=DATA.rows.filter(function(r){return r[1]===si&&r[2]===i&&ST.city.has(r[0])});
var c=calc(rs);fa.push(c.fact);fc.push(c.fc);oos.push(rs.some(function(r){return r[5]}))});
var mx=Math.max.apply(null,fa.concat(fc,[1])),W=300,H=110,step=W/Math.max(1,ms.length);
var y=function(v){return H-6-v/mx*(H-16)},s='';
fa.forEach(function(v,k){s+='<rect x="'+(k*step+2)+'" y="'+y(v)+'" width="'+(step-4)+'" height="'+(H-6-y(v))+'" fill="'+(oos[k]?'#E9A400':'#9fb3cf')+'"/>'});
s+='<polyline fill="none" stroke="#0E2B58" stroke-width="2" points="'+fc.map(function(v,k){return (k*step+step/2)+','+y(v)}).join(' ')+'"/>';
return '<div><h3>'+nm(DATA.skus[si])+'</h3><svg viewBox="0 0 '+W+' '+H+'" width="100%">'+s+'</svg></div>';
}
function render(){
document.documentElement.lang=LANG;
document.getElementById('ttl').textContent=T('Долина напитков · точность прогноза','Valley Beverages · forecast accuracy');
document.getElementById('lang').textContent=LANG==='ru'?'EN':'RU';
document.getElementById('why').textContent=T('Откуда цифра?','Where does this number come from?');
document.getElementById('csv').textContent=T('Скачать CSV','Download CSV');
side();
var k=calc(rows());window.KPI=k;
document.getElementById('kpis').innerHTML=
'<div class="kpi"><span>'+T('Факт','Actuals')+', '+nm(DATA.unit)+'</span><div>'+N(k.fact)+'</div></div>'+
'<div class="kpi"><span>'+T('Прогноз (лаг 1)','Forecast (lag 1)')+'</span><div>'+N(k.fc)+'</div></div>'+
'<div class="kpi"><span>'+T('Точность = 1 − WMAPE','Accuracy = 1 − WMAPE')+'</span><div>'+P(k.acc)+'</div></div>'+
'<div class="kpi"><span>BIAS</span><div class="'+(Math.abs(k.bias)>0.05?'bad':'')+'">'+P(k.bias)+'</div></div>';
document.getElementById('sm').innerHTML=[].concat.apply([],DATA.skus.map(function(s,i){return ST.sku.has(i)?[chart(i)]:[]})).join('')+
'<div class="note">'+T('Столбики — факт (жёлтые — месяц с дефицитом), линия — прогноз. Общая шкала внутри графика.','Bars — actuals (yellow — month with a shortage), line — forecast.')+'</div>';
var h='<tr><th>'+T('Город','City')+'</th><th>'+T('Товар','Product')+'</th><th>'+T('Факт','Actuals')+'</th><th>'+T('Прогноз','Forecast')+'</th><th>'+T('Точность','Accuracy')+'</th></tr>';
DATA.cities.forEach(function(c,ci){if(!ST.city.has(ci))return;DATA.skus.forEach(function(s,si){if(!ST.sku.has(si))return;
var x=calc(rows().filter(function(r){return r[0]===ci&&r[1]===si}));
h+='<tr><td>'+nm(c)+'</td><td>'+nm(s)+'</td><td>'+N(x.fact)+'</td><td>'+N(x.fc)+'</td><td'+(x.acc<0.85?' class="bad"':'')+'>'+P(x.acc)+'</td></tr>'})});
document.getElementById('tbl').innerHTML=h;
}
function explain(){
var k=calc(rows());
document.getElementById('explain').innerHTML=T(
'Точность = 1 − Σ|прогноз − факт| ÷ Σ факт = 1 − '+N(k.abs)+' ÷ '+N(k.fact)+' = '+P(k.acc)+'. Строк в расчёте: '+rows().length+' (город × товар × месяц). Источник: data.json, собран скриптом make_data.py.',
'Accuracy = 1 − Σ|forecast − actual| ÷ Σ actual = 1 − '+N(k.abs)+' ÷ '+N(k.fact)+' = '+P(k.acc)+'. Rows used: '+rows().length+' (city × product × month). Source: data.json built by make_data.py.');
}
function csv(){
var cs=DATA.cities.filter(function(c,i){return ST.city.has(i)}).map(nm),ss=DATA.skus.filter(function(s,i){return ST.sku.has(i)}).map(nm);
var lines=[[T('Город','City'),T('Товар','Product'),T('Месяц','Month'),T('Факт','Actuals'),T('Прогноз','Forecast')].join(';')];
rows().forEach(function(r){lines.push([nm(DATA.cities[r[0]]),nm(DATA.skus[r[1]]),DATA.months[r[2]],r[3],r[4]].join(';'))});
var name=T('Точность','Accuracy')+'_'+ST.year+'_'+(cs.length===DATA.cities.length?T('все города','all cities'):cs.join('+'))+'_'+(ss.length===DATA.skus.length?T('все товары','all products'):ss.join('+'))+'.csv';
var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([''+lines.join('\r\n')],{type:'text/csv;charset=utf-8'}));a.download=name;a.click();
}
document.getElementById('side').addEventListener('change',function(e){
var t=e.target,set=t.hasAttribute('data-c')?ST.city:ST.sku,i=+(t.getAttribute('data-c')||t.getAttribute('data-s'));
if(t.checked)set.add(i);else if(set.size>1)set.delete(i);else t.checked=true;
document.getElementById('explain').innerHTML='';render();
});
document.getElementById('year').addEventListener('change',function(e){ST.year=e.target.value;document.getElementById('explain').innerHTML='';render()});
document.getElementById('lang').addEventListener('click',function(){LANG=LANG==='ru'?'en':'ru';document.getElementById('explain').innerHTML='';render()});
document.getElementById('why').addEventListener('click',explain);
document.getElementById('csv').addEventListener('click',csv);
render();
</script>
</body>
</html> # -*- coding: utf-8 -*-
"""Сборка мини-дашборда в один HTML-файл / Builds the mini dashboard into one HTML file.
Что делает / What it does:
1) читает template.html (вёрстка + код) и data.json (цифры) / reads template.html and data.json;
2) проверяет повторные объявления в коде: две одинаковые const - пустая страница,
две одинаковые function - молча побеждает последняя / checks duplicate declarations;
3) вшивает данные в шаблон и пишет mini_dashboard.html / embeds data, writes mini_dashboard.html.
Файл открывается двойным кликом, интернет не нужен / Opens with a double click, no internet needed.
Запуск / Run: python build_dashboard.py (после / after: python make_data.py)
"""
import re
import sys
from collections import Counter
from pathlib import Path
HERE = Path(__file__).parent
OUT = HERE / "mini_dashboard.html"
def duplicates(code: str) -> list:
"""Имена верхнего уровня, объявленные дважды / Top-level names declared twice.
Смотрим только строки без отступа: переменные внутри функций могут повторяться.
Only unindented lines: variables inside functions may repeat.
"""
names = re.findall(r"^(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=", code, re.M)
names += ["function " + n for n in re.findall(r"^function\s+([A-Za-z_$][\w$]*)\s*\(", code, re.M)]
return [n for n, k in Counter(names).items() if k > 1]
def main() -> None:
tpl = (HERE / "template.html").read_text(encoding="utf-8")
data = (HERE / "data.json").read_text(encoding="utf-8")
code = "\n".join(re.findall(r"<script>(.*?)</script>", tpl, re.S))
dup = duplicates(code)
if dup:
print("ОШИБКА / ERROR: повторные объявления / duplicate declarations:", ", ".join(dup))
sys.exit(1)
if "/*DATA*/" not in tpl:
print("ОШИБКА / ERROR: в template.html нет метки /*DATA*/ / no /*DATA*/ marker in template.html")
sys.exit(1)
OUT.write_text(tpl.replace("/*DATA*/", data, 1), encoding="utf-8")
print(f"{OUT.name}: {OUT.stat().st_size / 1000:.0f} КБ / KB - готово / done")
if __name__ == "__main__":
main() # -*- coding: utf-8 -*-
"""Проверка мини-дашборда в браузере без окна / Checks the mini dashboard in a headless browser.
Что делает / What it does:
открывает mini_dashboard.html в Microsoft Edge или Google Chrome без окна, переключает год и язык,
собирает ошибки JavaScript и сверяет точность на экране с расчётом Python по data.json.
opens mini_dashboard.html in headless Edge or Chrome, switches year and language,
collects JavaScript errors and reconciles the on-screen accuracy with a Python calculation.
Код выхода 1, если что-то не так / Exit code 1 if anything is wrong.
Запуск / Run: python check_dashboard.py
"""
import html as h
import json
import re
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).parent
PAGE = HERE / "mini_dashboard.html"
TMP = HERE / "_check.html"
BROWSERS = [Path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"),
Path(r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"),
Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
Path(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe")]
TRAP = "<script>window.__E=[];window.onerror=function(m,u,l){__E.push(m+' @'+l)};</script>"
TEST = """<script>window.addEventListener('load',function(){setTimeout(function(){var o={err:__E};
try{o.acc26=KPI.acc;o.fact26=KPI.fact;var y=document.getElementById('year');y.value='2025';y.dispatchEvent(new Event('change'));
o.acc25=KPI.acc;document.getElementById('lang').click();o.ttl=document.getElementById('ttl').textContent;
document.getElementById('why').click();o.explain=document.getElementById('explain').textContent.length;
o.nan=/NaN|undefined/.test(document.querySelector('main').innerHTML)}catch(e){o.fatal=String(e)}
var p=document.createElement('pre');p.id='OUT';p.textContent=JSON.stringify(o);document.body.appendChild(p)},300)});</script>"""
def accuracy(data: dict, year: str) -> float:
"""Точность по всем строкам года / Accuracy over all rows of the year."""
rs = [r for r in data["rows"] if data["months"][r[2]].startswith(year)]
fact = sum(r[3] for r in rs)
return max(0.0, 1 - sum(abs(r[4] - r[3]) for r in rs) / fact)
def main() -> None:
browser = next((p for p in BROWSERS if p.exists()), None)
if not browser:
print("ОШИБКА / ERROR: не найден Edge или Chrome / Edge or Chrome not found")
sys.exit(1)
page = PAGE.read_text(encoding="utf-8")
head, tail = page.rsplit("</body>", 1)
TMP.write_text(head.replace("<head>", "<head>" + TRAP, 1) + TEST + "</body>" + tail, encoding="utf-8")
try:
r = subprocess.run([str(browser), "--headless=new", "--disable-gpu", "--virtual-time-budget=20000",
"--dump-dom", TMP.as_uri()], capture_output=True, timeout=180)
finally:
TMP.unlink(missing_ok=True)
m = re.search(r'<pre id="OUT">(.*?)</pre>', r.stdout.decode("utf-8", "replace"), re.S)
if not m:
print("ОШИБКА / ERROR: страница не вернула отчёт / the page returned no report")
sys.exit(1)
out = json.loads(h.unescape(m.group(1)))
data = json.loads((HERE / "data.json").read_text(encoding="utf-8"))
ok26 = abs(out.get("acc26", -1) - accuracy(data, "2026")) < 1e-9
ok25 = abs(out.get("acc25", -1) - accuracy(data, "2025")) < 1e-9
print("ошибки JS / JS errors:", out["err"] or "нет / none", "| fatal:", out.get("fatal", "нет / none"))
print(f"точность 2026 / accuracy 2026: экран / screen {out.get('acc26', 0):.4f}, Python {accuracy(data, '2026'):.4f} -> {'OK' if ok26 else 'НЕ СХОДИТСЯ / MISMATCH'}")
print(f"точность 2025 / accuracy 2025: экран / screen {out.get('acc25', 0):.4f}, Python {accuracy(data, '2025'):.4f} -> {'OK' if ok25 else 'НЕ СХОДИТСЯ / MISMATCH'}")
print("заголовок после EN / title after EN:", out.get("ttl"), "| NaN/undefined:", out.get("nan"))
bad = out["err"] or out.get("fatal") or not ok26 or not ok25 or out.get("nan") or not out.get("explain")
print("ИТОГ / RESULT:", "ЕСТЬ ОШИБКИ / ERRORS" if bad else "OK")
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main() @echo off rem Builds the mini dashboard and opens it. ASCII only: Cyrillic breaks cmd. cd /d "%~dp0" where python >nul 2>nul || goto nopython python make_data.py || goto err python build_dashboard.py || goto err start "" "mini_dashboard.html" exit /b 0 :nopython echo ERROR: python not found. Install Python 3.10+ from python.org and tick "Add python.exe to PATH". pause exit /b 1 :err echo ERROR: see the messages above. pause exit /b 1