Авторский курс Елизаровой НаталииAuthor's course by Nataliia Elizarova
Модуль 5 · Урок 05-18 · Новый урокModule 5 · Lesson 05-18 · New lesson

Свой дашборд: как он устроен и образец для скачиванияYour own dashboard: how it works and a sample to download

В модуле вы считали точность, смещение и 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.

🆕 Новый урок🆕 New lesson ⏱ 40 минут⏱ 40 minutes 🧩 Тренажёр «Соберите дашборд»🧩 Simulator: "Build the dashboard" ⬇ Образец и стартовый набор⬇ Sample and starter kit ✅ Тест из 8 вопросов✅ An 8-question quiz

После урока вы сможетеAfter this lesson you will be able to

  • объяснить, из каких пяти шагов собирается дашборд-файл и что ломается, если шаг пропустить;
  • сравнить свой дашборд с Power BI и Excel и выбрать инструмент под задачу;
  • пройти по листам образца «Долины напитков» и найти встроенные ловушки данных;
  • скачать стартовый набор и понять, какой файл за что отвечает;
  • применить восемь правил, которые мы выучили на рабочих дашбордах.
  • explain the five steps that build a dashboard file and what breaks if a step is skipped;
  • compare your own dashboard with Power BI and Excel and pick the tool for the job;
  • walk through the sheets of the Valley Beverages sample and find the data traps built into it;
  • download the starter kit and understand which file does what;
  • apply the eight rules we learned on working dashboards.
1

Пять слов урокаFive words of the lesson

HTMLформат веб-страницы; один файл .html открывает любой браузер — Edge, Chromethe web page format; any browser — Edge, Chrome — opens a single .html file
data.jsonфайл с цифрами в виде, удобном для кода; его готовит скрипт Pythona file with the numbers in a code-friendly form; a Python script prepares it
СборкаBuildскрипт вшивает цифры и код в один HTML-файлa script stitches the numbers and the code into one HTML file
ПроверкаCheckскрипт открывает дашборд в браузере без окна и ищет ошибки и расхождения цифрa script opens the dashboard in a windowless browser and looks for errors and number mismatches
ПубликацияPublishingготовый файл кладут туда, где его найдёт команда: общая папка, порталthe finished file goes where the team will find it: a shared folder, a portal
2

На пальцах: кухня ресторанаIn plain words: a restaurant kitchen

Дашборд готовится как блюдо. Есть продукты — выгрузки продаж и прогноза. Есть рецепт — шаблон с вёрсткой и формулами. Повар по рецепту собирает блюдо — скрипт сборки. Перед подачей шеф пробует — скрипт проверки. И официант несёт гостям — публикация. Если пропустить дегустацию, гость первым узнает, что суп пересолен.

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.

Формула урокаThe lesson formula Дашборд = данные + шаблон → сборка → проверка → публикация. Правите данные или шаблон — всегда заново собираете и проверяете. Пропущенный шаг не видно, пока не станет поздно. Dashboard = data + template → build → check → publish. Change the data or the template — always rebuild and recheck. A skipped step is invisible until it is too late.
3

Зачем свой дашборд, если есть Power BI и ExcelWhy your own dashboard when Power BI and Excel exist

Свой HTML-файлOwn HTML filePower 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 CodeBI-команда или вы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.

4

Как устроен: пять шагов и пять файловHow it works: five steps and five files

ШагStepФайл в стартовом набореFile in the starter kitЧто на входе → что на выходеInput → output
1. Данные1. Datamake_data.pyвыгрузки (в наборе — учебный генератор) → data.jsonexports (a training generator in the kit) → data.json
2. Шаблон2. Templatetemplate.htmlвёрстка, фильтры, карточки, графики, расчёт точности; метка /*DATA*/layout, filters, cards, charts, accuracy maths; the /*DATA*/ marker
3. Сборка3. Buildbuild_dashboard.pyшаблон + data.json → mini_dashboard.html; по пути ищет повторные объявленияtemplate + data.json → mini_dashboard.html; checks for duplicate declarations on the way
4. Проверка4. Checkcheck_dashboard.pyдашборд в браузере без окна → ошибки JS и сверка точности с Pythondashboard in a windowless browser → JS errors and accuracy reconciled with Python
5. Публикация5. Publishrun_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.

5

Тренажёр: соберите дашбордSimulator: build the dashboard

Нажмите на шаг, чтобы выключить или включить его. Под цепочкой — что увидит команда. Выберите и ситуацию: «первый запуск» или «правка после замечания».

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".

Что показал тренажёрWhat the simulator showed Самая коварная поломка — не пустой экран, а правдоподобный экран со старыми цифрами. Её даёт пропущенная пересборка после правки. Ловит её только сверка цифры на экране с расчётом. The most treacherous failure is not a blank screen but a plausible screen with old numbers. A skipped rebuild after an edit causes it. Only reconciling the on-screen number with a calculation catches it.
6

Образец «Долины напитков»: 14 листовThe Valley Beverages sample: 14 sheets

Большой образец повторяет устройство наших рабочих дашбордов к встрече 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 page06-07
Страна 1 / 2 / 3Country 1 / 2 / 3города и товары страны, деревья с раскрытиемthe country's cities and products, expandable trees05-10
КомментарииCommentsпричины отклонений рядом с цифрамиreasons for deviations next to the numbers05-11
КаналыChannelsTraditional / Modern Trade, HoReCa, E-commerceTraditional / Modern Trade, HoReCa, E-commerce02-04
АналитикаAnalyticsдинамика и структура портфеляtrends and portfolio structure05-17
Изменения прогнозаForecast changesчто и насколько изменилось против прошлого циклаwhat changed versus the previous cycle and by how much05-04
YTGостаток года: факт + прогноз против планаyear to go: actuals + forecast versus plan06-07
Текущий месяцMonth to dateкак закроем месяц по темпу днейhow the month will close at the daily pace06-09
Точность (FA)Accuracy (FA)точность по лагу, уровню и методике, склейка кодовaccuracy by lag, level and method, code glue05-06, 05-10
Сводка DRMDRM summaryриски, возможности и решенияrisks, opportunities and decisions06-01
Building blocksиз чего складывается рост: цена, точки, новинки, промоwhat growth is made of: price, outlets, launches, promos06-08
Колода DRMDRM deck12 слайдов с заголовком-выводом, выгрузка в .pptx12 slides with takeaway headlines, export to .pptx05-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.

7

Ловушки, встроенные в образецTraps built into the sample

В учебные данные специально заложены те же ловушки, что встречаются в жизни. Найдите каждую в дашборде — это и есть задание раздела.

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 temperamentsYTG, Изменения прогноза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%.

8

Скачать: образец и стартовый наборDownload: the sample and the starter kit

Образец — чтобы посмотреть и покрутить. Стартовый набор — чтобы собрать свой: 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.

📊 Образец «Долина напитков» (DRM)📊 Valley Beverages sample (DRM)Один HTML-файл 2,25 МБ, RU/EN, 14 листов. Открыть двойным кликом.One 2.25 MB HTML file, RU/EN, 14 sheets. Open with a double click.
📘 README.mdЧто в наборе и как запустить.What is in the kit and how to run it.
🧪 make_data.pyУчебные данные → data.json.Training data → data.json.
🎨 template.htmlВёрстка и код дашборда.The dashboard layout and code.
🔧 build_dashboard.pyСборка в один файл + поиск повторных объявлений.Build into one file + duplicate-declaration check.
✅ check_dashboard.pyПроверка в Edge или Chrome без окна + сверка с Python.Check in windowless Edge or Chrome + reconciliation with Python.
▶ run_dashboard.batКнопка: данные → сборка → открыть. Браузер может переспросить про .bat — это нормально.A button: data → build → open. The browser may ask about the .bat — that is normal.

Тексты файлов можно открыть прямо здесь и скопировать — в последней части урока.

You can open the file texts right here and copy them — in the last part of the lesson.

9

Кто что делаетWho does what

ПланерPlannerзнает, какие цифры нужны встрече, ставит задачу, сверяет цифры с эталоном, отвечает за результатknows which numbers the meeting needs, sets the task, reconciles numbers with a reference, owns the result
Claude Codeпишет и правит код шаблона и скриптов по вашему заданию; сам ничего не публикуетwrites and edits the template and scripts to your spec; publishes nothing by itself
Pythonсчитает и собирает: готовит data.json, вшивает в HTML, гоняет проверкуcalculates and builds: prepares data.json, embeds it into HTML, runs the check
БраузерBrowserпоказывает дашборд и пересчитывает цифры при смене фильтров — на компьютере зрителяshows the dashboard and recalculates numbers when filters change — on the viewer's computer

Важно: 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.

10

Восемь правил, которые мы выучили на рабочих дашбордахEight rules we learned on working dashboards

Правило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
11

«Откуда цифра?» без искусственного интеллекта внутри"Where does this number come from?" without AI inside

Хочется добавить в дашборд «умного помощника», который отвечает на вопросы. Для этого нужен ключ доступа к сервису ИИ. Но 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.

Так отвечает мини-дашбордHow the mini dashboard answers «Точность = 1 − Σ|прогноз − факт| ÷ Σ факт = 1 − 2 818 ÷ 40 465 = 93,0 %. Строк в расчёте: 108 (город × товар × месяц). Источник: data.json, собран скриптом make_data.py.» — цифры для 2026 года, все города и товары. "Accuracy = 1 − Σ|forecast − actual| ÷ Σ actual = 1 − 2,818 ÷ 40,465 = 93.0%. Rows used: 108 (city × product × month). Source: data.json built by make_data.py." — numbers for 2026, all cities and products.
12

Данные внутри файла: что можно показыватьData inside the file: what you may share

Главное ограничениеThe main limitation Всё, что вшито в дашборд, получает каждый, у кого есть файл, — даже то, что скрыто фильтром. Разослали — уже не отозвать. Поэтому рабочие дашборды живут в папках компании с доступом по ролям, а наружу (курс, конференция, резюме) — только учебные или обезличенные данные, как в «Долине напитков». Everything embedded in the dashboard goes to everyone who has the file — even what a filter hides. Once sent, it cannot be recalled. So working dashboards live in company folders with role-based access, and only training or anonymised data goes outside (a course, a conference, a CV), like Valley Beverages.
13

Как вы делаете руками ↔ как это называетсяWhat you do by hand ↔ what it is called

РукамиBy handКак называетсяWhat it is called
скачали выгрузки, свели в одну таблицуdownloaded the exports, merged them into one tableETL — извлечь, преобразовать, загрузить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
14

Визуальная проверка обязательнаVisual check is mandatory

Визуальная проверка обязательна. Скрипт ловит ошибки кода и расхождения цифр, но не видит, что график «не про то», подпись обрезана, а в английской версии остался русский текст. Перед публикацией откройте дашборд сами: пройдите все листы, переключите язык, поставьте два-три фильтра, сверьте одну цифру с эталоном.

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.

15

Что нового в 2025–2026What is new in 2025–2026

🧑‍💻

Claude Code: установщик и приложениеClaude Code: installer and app

Для 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.

📈

Агенты в ExcelAgents in Excel

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.

Правило для любой новинкиThe rule for any novelty Новый инструмент берём, только если на ваших данных он даёт такие же цифры, как эталонный расчёт, и экономит время. Любую новинку — против «как год назад» на своих данных. Adopt a new tool only if, on your data, it gives the same numbers as the reference calculation and saves time. Any novelty — against "same as last year" on your own data.

Источники: документация 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.

16

Как это бывает в жизниHow it happens in real life

Три случая из работы планера в компании-производителе напитков. Названия изменены.

Three cases from planning work at a beverage manufacturer. Names are changed.

1. Исправили, но не пересобрали1. Fixed, but not rebuilt

Что случилосьWhat happened
В скрипте подготовки данных исправили ошибку, проверили data.json — цифры верные. А на встрече дашборд показал старые.A bug in the data-prep script was fixed and data.json checked — the numbers were right. But at the meeting the dashboard showed the old ones.
Что на самом делеWhat was really going on
HTML не пересобрали: в нём были вшиты прежние цифры.The HTML was not rebuilt: it still had the previous numbers embedded.
ПравилоRule
Правка → сборка → проверка. Сверяем цифру на экране, а не в промежуточном файле.Edit → build → check. Reconcile the number on screen, not in an intermediate file.

2. Пустая страница у всей команды2. A blank page for the whole team

Что случилосьWhat happened
После добавления нового листа дашборд открылся белым экраном.After a new sheet was added the dashboard opened as a white screen.
Что на самом делеWhat was really going on
Одна и та же константа объявлена дважды — браузер останавливает весь код.The same constant was declared twice — the browser stops all the code.
ПравилоRule
Проверка повторных объявлений — часть сборки, а не «если вспомним».The duplicate-declaration check is part of the build, not "if we remember".

3. «Готово», а файла нет3. "Done", but no file

Что случилосьWhat happened
Скрипт обновления написал «готово», но в общей папке лежал вчерашний дашборд.The update script said "done", but yesterday's dashboard was in the shared folder.
Что на самом делеWhat was really going on
Один из файлов был открыт в Excel; ошибка записи утащила за собой копирование, а сообщение «готово» было написано заранее.One of the files was open in Excel; the write error took the copy step down with it, and "done" had been printed in advance.
ПравилоRule
«Готово» — только после проверки, что файл действительно записан. Подробнее — 05-21."Done" only after checking the file was actually written. More in 05-21.
17

Практика в ExcelExcel practice

В книге: «выгрузка» — 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.

ЗаданияTasks

  1. На листе «итоги» найдите точность и BIAS. Совпадают ли формулы с теми, что в кнопке «Откуда цифра?» мини-дашборда?
  2. Посчитайте точность только по Ветровску. Выше она или ниже общей?
  3. По «словарю данных» ответьте: какой лист образца нужен, чтобы понять, как закроется текущий месяц, и как он считает?
  4. Скачайте стартовый набор и разложите шесть файлов по пяти шагам из раздела 4.
  1. On the "totals" sheet find accuracy and BIAS. Do the formulas match the ones behind the mini dashboard's "Where does this number come from?" button?
  2. Calculate accuracy for Vetrovsk only. Is it higher or lower than the total?
  3. Using the "data dictionary": which sample sheet tells you how the current month will close, and how does it calculate it?
  4. Download the starter kit and sort the six files into the five steps from section 4.
ОтветыAnswers

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.

18

Частые вопросыFAQ

Кто здесь что делает — Claude, Python или я?Who does what here — Claude, Python or me?

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.

Нужно ли уметь программировать?Do I need to know how to code?

Писать код — нет. Читать сообщения об ошибках, запускать команды и проверять цифры — да. Этому и посвящён урок 05-19.

Writing code — no. Reading error messages, running commands and checking numbers — yes. That is what lesson 05-19 is about.

Почему образец 2,25 МБ, а мини — 15 КБ?Why is the sample 2.25 MB and the mini 15 KB?

В образец вшиты три библиотеки: 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.

Можно ли открыть дашборд на телефоне?Can I open the dashboard on a phone?

Да, если файл доступен на телефоне: в шаблоне есть раскладка для узкого экрана. Но проверяйте — длинные таблицы на телефоне читать трудно.

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.

19

Упражнение: какой файл чинить?Exercise: which file to fix?

20

Три мифаThree myths

Миф:Myth: свой дашборд — это для программистов.your own dashboard is for programmers.
На самом деле:In fact: код пишет ассистент; от планера нужны задача, проверка цифр и порядок шагов.the assistant writes the code; the planner brings the task, the number checks and the order of steps.
Миф:Myth: открылся без ошибок — значит, цифры верные.it opened without errors — so the numbers are right.
На самом деле:In fact: самая частая беда — правдоподобные старые цифры. Спасает только сверка.the most common trouble is plausible old numbers. Only reconciliation helps.
Миф:Myth: добавим ИИ-чат прямо в дашборд — будет удобно.let's add an AI chat right into the dashboard — it will be handy.
На самом деле:In fact: ключ доступа в файле увидит любой. Объяснение цифры дашборд считает сам.anyone would see the access key in the file. The dashboard computes the number's explanation itself.
21

Что запомнить: отметьте галочкойWhat to remember: tick it off

22

Проверьте себяCheck yourself

23

Шпаргалка терминовGlossary

HTMLформат веб-страницы, открывается любым браузеромthe web page format, any browser opens it
JSONтекстовый формат данных для кодаa text data format for code
Templateшаблон: вёрстка и код без цифрlayout and code without the numbers
Buildсборка: шаблон + данные → один файлtemplate + data → one file
Headless browserбраузер без окна для автоматической проверкиa windowless browser for automated checks
Smoke testбыстрая проверка «открывается и не падает»a quick "opens and does not crash" check
Reconciliationсверка цифры на экране с независимым расчётомmatching the on-screen number with an independent calculation
API keyключ доступа к сервису; в дашборд не кладёмa service access key; never in a dashboard
ETLизвлечь, преобразовать, загрузить данныеextract, transform, load
Итог урокаLesson summary Свой дашборд — это один HTML-файл, собранный из данных и шаблона. Пять шагов: данные → шаблон → сборка → проверка → публикация; после любой правки — пересборка и сверка цифры на экране. Образец «Долины напитков» показывает 14 листов и встроенные ловушки, стартовый набор из 6 файлов вы соберёте в уроке 05-19. Подключение своих файлов — 05-20, обновление и публикация — 05-21. Your own dashboard is one HTML file built from data and a template. Five steps: data → template → build → check → publish; after any edit — rebuild and reconcile the on-screen number. The Valley Beverages sample shows 14 sheets and built-in traps; you will build the 6-file starter kit in lesson 05-19. Connecting your own files — 05-20, refreshing and publishing — 05-21.
24

Тексты файлов стартового набораStarter kit file texts

Те же тексты, что скачиваются кнопками в разделе 8. Можно скопировать вручную.

The same texts the buttons in section 8 download. You can copy them by hand.

README.md
# Мини-дашборд «Долина напитков» — стартовый набор
Учебный набор из уроков 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.
make_data.py
# -*- 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()
template.html
<!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>
build_dashboard.py
# -*- 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()
check_dashboard.py
# -*- 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()
run_dashboard.bat
@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
Made on
Tilda