До списку уроків
9 клас · Урок 12 з 16

Дашборд, який оновлюється сам: cron, логи, свіжість

виконано0%
Крок 1

Що робимо сьогодні

Модуль I · IT Skills (Cloud Services & Documents) · тема 6, урок 2 з 2 · 120 хвилин

Сьогодні ти поставиш свій скрипт у розклад. Дашборд почне оновлюватися без тебе — навіть уночі й на канікулах.

А ще ти навчишся ловити автоматику на брехні. Бо ламається вона тихо: сторінка виглядає так само гарно, тільки числа на ній учорашні.

На минулому уроці ти написав ~/bin/build_dash.py: він читає ~/data/responses.csv і кладе метрики в ~/www/dash/data.json. Поки що цей скрипт запускаєш ти — руками, коли згадаєш. Сьогодні його запускатиме cron. А ти навчишся бачити, що саме він робить. Навіть коли тебе немає за комп’ютером.

Головний тест цього уроку Наприкінці уроку сервер видалить твій data.json і зачекає. Якщо через 16 хвилин файл з’явиться сам і дата всередині буде свіжою — автоматика працює. Якщо ні — вона не працює, як би переконливо не виглядала сторінка. Це єдиний чесний доказ.

Після уроку ти вмієш

Слова cron, crontab і «лог» поки нічого не означають. Розберемо їх по черзі.

Усе це — на сервері, по SSH crontab — програма Linux. У PowerShell на твоєму комп’ютері її немає, і команди з цієї сторінки там не працюють. Локально ти сьогодні тільки верстаєш index.html і dash.css; усе інше — у SSH-сесії на своєму акаунті.

Хід уроку

ЕтапХвЩо робимо
Розминка10Перевірка п’ятого KPI з домашнього завдання
Cron: п’ять полів20Тренажер розкладу і конструктор crontab
Логи й повторні запуски15Симуляція падіння на третьому запуску
Практика: запуск за розкладом25crontab -e, абсолютні шляхи, перший запис у лозі
Перерва10
Свіжість даних15Плашка, яка червоніє; «Дашборд-детектив»
Практика: сторінка дашборда204 картки KPI, 2 графіки, плашка віку даних
Тест на автоматизацію5Учитель видаляє data.json — чекаємо
Разом120
Що ти здаєш Запис у crontab — не рідше ніж раз на 15 хвилин, з абсолютними шляхами і перенаправленням у лог. Файл ~/logs/dash.log із мітками часу. Сторінка ~/www/dash/index.html: 4 картки .kpi, два графіки Chart.js із data.json, плашка «оновлено N хвилин тому». І стилі до неї — ~/www/dash/dash.css.
Чому тека зветься dash, а не lesson-12 Теки практики звуться за темою, а не за номером уроку. Тема 6 — це уроки 11 і 12, і обидва складають роботу в одну теку ~/www/dash/. Скрипт із минулого уроку лишається там, де й був: ~/bin/build_dash.py.
Крок 2

Cron: п’ять полів, які вирішують, коли

Служба, яка щохвилини звіряє час і запускає те, чому час настав

У школі дзвінок на урок дає не вчитель. Є розклад, і дзвінок лунає сам — о 8:30, о 9:25, о 10:20. Ніхто щоразу не натискає кнопку.

На сервері за таке відповідає cron. Це служба, яка працює постійно. Раз на хвилину вона переглядає списки завдань усіх користувачів. І запускає ті рядки, чий розклад збігся з поточною хвилиною.

Твій особистий список зветься crontab — «cron table», таблиця cron.

Кожен рядок crontab — це п’ять полів розкладу. Далі, до кінця рядка, іде команда. Пробіли між полями можуть бути будь-якої довжини. А от полів рівно п’ять. Cron рахує їх від початку рядка. Усе, що після п’ятого, він вважає командою.

* * * * * /usr/bin/python3 /home/student/bin/build_dash.py п’ять полів розкладу — коли команда — що саме запустити 12 34 5 хвилина година день місяця місяць день тижня 0–59 0–23 1–31 1–12 0–7 30 → о :30 */15 → :00 :15 7 → сьома 9-17 → з 9 до 17 1 → перше число 1,15 → 1-е і 15-е 9 → вересень або JAN…DEC 1 → понеділок 0 і 7 → неділя Спецсимволи в будь-якому полі * будь-яке значення , перелік: 0,30 - діапазон: 9-17 / крок: */15, 9-17/2 Якщо обидва ці поля задані (не «*»), cron запускає завдання, коли збігається хоча б одне з них — це «або», а не «і».
Числа в полях — це не «через скільки», а «в яку саме хвилину доби». Cron не рахує час від моменту, коли ти зберіг файл: він щохвилини звіряє поточний час із твоїм шаблоном.

Дві команди, які потрібні щодня

crontab -e відкриває твій особистий список у редакторі. Після збереження cron одразу перечитує його. Перезапускати нічого не треба.

А crontab -l друкує список на екран, нічого не змінюючи.

сервер, ssh
$ EDITOR=nano crontab -e crontab: installing new crontab $ crontab -l */15 * * * * /usr/bin/python3 /home/student/bin/build_dash.py >> /home/student/logs/dash.log 2>&1
Одна літера, яка стирає все crontab -r видаляє твій crontab без жодного запитання. Клавіші e і r поруч. Перше, що робиш після налаштування: crontab -l > ~/notes/crontab.bak — тоді відновлення займе одну команду, а не пів уроку.

Скорочення замість п’яти полів

Замість п’яти полів можна написати скорочення: @daily, @hourly, @weekly, @monthly, @yearly. Наприклад, @daily — це рівно 0 0 * * *, тобто опівночі.

Окремо стоїть @reboot. Це не розклад, а «один раз після старту сервера». Для дашборда він не підходить: сервер може не перезавантажуватися місяцями.

Питання

Однокласник хотів «щодня о 7:30» і написав 7 30 * * *. Скрипт не спрацював о 7:30, а crontab -e при збереженні вилаявся. Що сталося?

Питання

У чужому crontab ти бачиш рядок 0 12 13 * 5. Автор каже: «це щоб раз на рік у п’ятницю 13-го надіслати вітання». Коли він спрацює насправді?

Крок 3

Що насправді означає */15

Найпоширеніша хибна модель: «кожні 15 хвилин від моменту, коли я зберіг»

Скісна риска — це крок усередині діапазону поля, а не таймер. Запис */15 у полі хвилин читається так. Візьми весь діапазон 0–59. Залиш кожне п’ятнадцяте значення. Виходить 0, 15, 30, 45.

Ці хвилини прибиті до годинника, а не до тебе.

Зберіг crontab о 10:07? Перший запуск буде о 10:15. Не о 10:22. А зберіг о 10:14 — запуск уже через хвилину. Cron не знає й не хоче знати, коли ти натиснув «зберегти».

*/15 * * * * кожні 15 хвилин: :00 :15 :30 :45 0 * * * * щогодини рівно о нулях 30 7 * * * один раз на добу */7 * * * * крок не ділить годину націло — на межі години розрив ламається 07:00 08:00 09:00
Дві години реального часу. У розкладі */7 хвилини йдуть 0, 7, 14, 21, 28, 35, 42, 49, 56 — і далі знову 0. Червона ділянка — той самий проміжок: між :56 і :00 минає не 7 хвилин, а 4. Тому кроки беруть такі, що ділять 60 націло: 5, 10, 15, 20, 30.
ЗаписКоли спрацюєСкільки разів на добу
* * * * *щохвилини1440
*/15 * * * *о :00, :15, :30, :45 кожної години96
0 * * * *щогодини рівно о нулях24
*/5 9-17 * * 1-5кожні 5 хв з 9:00 до 17:59 у робочі дні108
30 7 * * *щодня о 7:301
0 6 * * 1щопонеділка о 6:001 на тиждень
5 0 1 * *першого числа кожного місяця о 0:051 на місяць
Раз на 15 хвилин — це 96 запусків на добу Кожен запуск щось читає й пише. Скрипт, який працює 0,3 секунди, — це нормально. А скрипт, який працює 4 хвилини, при розкладі «кожні 5 хвилин» одного дня наздожене сам себе. Тоді два його примірники писатимуть той самий файл одночасно. Чим частіший розклад, тим коротшою має бути робота.
Питання

О 10:07 ти зберіг */15 * * * *. Дивишся на годинник о 10:20 — у лозі один запис. Об 11:00 — скільки їх буде всього?

Крок 4

Конструктор crontab

Крути п’ять полів — сторінка перекладе розклад людською мовою і покаже п’ять наступних запусків справжніми датами

Правило перевірки просте. Спробуй прочитати свій рядок вголос однією фразою. Не виходить — він, найімовірніше, робить не те, що ти думаєш. Тож спочатку формулюй фразу, а потім складай поля.

Тренажер розкладу
хвилина
година
день місяця
місяць
день тижня

Наступні п’ять запусків від цієї хвилини:

    Готовий рядок для crontab -e — з абсолютними шляхами і перенаправленням у лог:

    рядок crontab

    Розбір готового рядка

    ЧастинаНавіщо саме так
    /usr/bin/python3 абсолютний шлях до інтерпретатора. Просто python3 може не знайтися: у cron свій, дуже короткий PATH
    /home/student/bin/build_dash.py абсолютний шлях до скрипта. ~/bin/… залежить від оболонки, bin/… — від поточної теки
    >> дописати в кінець файлу. Одна риска > щоразу стирала б лог, і ти б бачив тільки останній запуск
    2>&1 відправити помилки туди ж, куди й звичайний вивід. Без цього лог буде порожній саме тоді, коли він найпотрібніший
    Знак відсотка в crontab означає не те, що в оболонці У рядку crontab % — спецсимвол: cron перетворює його на перенесення рядка і все, що після першого %, віддає команді на вхід. Тому date +%Y-%m у crontab розвалюється. Кожен відсоток екранують зворотним слешем: date +\%Y-\%m. У самому скрипті на Python цієї проблеми немає — вона тільки в рядку crontab.
    Питання

    Дашборд має оновлюватися кожні 10 хвилин, але тільки в шкільний час. Це з 8:00 до 16:59 у робочі дні. Який рядок правильний?

    Крок 5

    «У мене вручну працює, а за розкладом ні»

    Причина майже завжди одна: cron працює в іншому оточенні

    Коли ти набираєш команду в SSH-сесії, її виконує твоя оболонка. Перед стартом вона багато чого встигла. Прочитала ~/.bashrc. Зібрала довгий PATH. Підхопила мову й віртуальне середовище Python. І стоїть вона в тій теці, де стоїш ти.

    Cron не робить нічого з цього. Він не входить у систему, не читає твоїх конфігів, не є інтерактивною оболонкою. Він бере /bin/sh, дає йому кілька змінних — і все.

    твоя сесія: ssh + bash завдання cron PATH=/home/student/.local/bin: /usr/local/bin:/usr/bin:/bin SHELL=/bin/bash HOME=/home/student LANG=uk_UA.UTF-8 поточна тека — де ти стоїш ~/.bashrc прочитано аліаси, функції, venv — на місці PATH=/usr/bin:/bin і більше нічого SHELL=/bin/sh HOME=/home/student LANG не задано поточна тека — завжди /home/student ~/.bashrc НЕ прочитано аліасів, функцій і venv немає усе, що жило у твоєму PATH ліворуч і не потрапило в /usr/bin чи /bin, для cron просто не існує
    Значення PATH у cron задає сама служба; у більшості дистрибутивів це /usr/bin:/bin. Перевіряти напам’ять не треба — нижче є спосіб подивитися своє.

    Розвідка: подивись оточення cron власними очима

    Не вір таблиці, вір своєму серверу. Постав на одну хвилину рядок, який вивалить усе оточення у файл. Зачекай хвилину. Потім прибери цей рядок.

    тимчасовий рядок у crontab
    * * * * * /usr/bin/env > /home/student/logs/cron-env.txt 2>&1
    через хвилину — порівняння
    $ cat ~/logs/cron-env.txt HOME=/home/student LOGNAME=student PATH=/usr/bin:/bin SHELL=/bin/sh PWD=/home/student $ echo $PATH /home/student/.local/bin:/usr/local/bin:/usr/bin:/bin

    Точний список у твоєму акаунті може відрізнятися — важливо не запам’ятати рядки, а побачити, наскільки він коротший за твій.

    Три правила, які знімають більшість проблем

    ПравилоПоганоДобре
    Абсолютний шлях до програми python3 build_dash.py /usr/bin/python3 /home/student/bin/build_dash.py
    Абсолютні шляхи всередині скрипта open('data/responses.csv') open(Path.home()/'data'/'responses.csv')
    Свій вивід — у свій файл нічого не перенаправлено >> /home/student/logs/dash.log 2>&1

    Абсолютний шлях до python3 легко дізнатися: which python3 друкує саме те, що треба підставити. Абсолютний шлях до свого скрипта — readlink -f ~/bin/build_dash.py.

    Найпідступніший випадок: скрипт відпрацював «успішно» Поточна тека завдання cron — домашня, а не та, де лежить скрипт. Якщо всередині написано open('dash/data.json', 'w'), файл спокійно створиться в /home/student/dash/data.json. Скрипт завершиться кодом 0, у лозі буде «OK», а сторінка місяцями показуватиме старі числа. Зелений лог не доводить, що дані оновилися — доводить тільки дата у файлі, який реально читає сторінка.

    Куди зникає вивід, якщо його не перенаправити

    Cron не викидає те, що надрукувала твоя команда. Він намагається надіслати це поштою власникові завдання. Адресу можна змінити рядком MAILTO= на початку crontab.

    На навчальному сервері поштова служба зазвичай не налаштована. Вивід тихо зникає. Звідси й відчуття «нічого не сталося». Насправді сталося — просто повідомлення пішло в нікуди.

    Питання

    Вручну ~/bin/build_dash.py відпрацьовує за пів секунди. За розкладом — нічого: data.json не оновлюється, а ~/logs/dash.log порожній, у ньому взагалі жодного рядка. З чого почати?

    Питання

    Лог заповнюється справно: кожні 15 хвилин «START», потім «OK, 52 записи». Але сторінка дашборда третій день показує ті самі числа. І поле generated_at у ній не змінюється. Де шукати?

    Крок 6

    Лог: єдине, що лишається після скрипта

    Скрипт, який мовчить, неможливо ні перевірити, ні полагодити

    Коли ти запускаєш програму руками, ти бачиш її вивід. Коли її запускає cron, ти не бачиш нічого. Скільки вона працювала? Скільки записів обробила? Чи впала взагалі? Відповідей немає.

    Лог — це твій єдиний спосіб дізнатися, що відбувалося о 3:15 ночі.

    Мінімальний корисний лог

    Скрипту не треба вміти писати у файл. Йому досить print(). Перенаправлення >> у рядку crontab саме складе вивід у файл.

    Зате кожен рядок мусить мати мітку часу. Без неї лог — купа однакових речень, з якої нічого не дізнаєшся.

    ~/bin/build_dash.py — фрагмент
    from datetime import datetime def log(msg): ts = datetime.now().astimezone().isoformat(timespec='seconds') print(f'{ts} {msg}', flush=True) log('START build_dash.py') try: rows = read_csv(CSV) data = build_metrics(rows) write_atomic(OUT, data) log(f'OK {len(rows)} записів, {len(data["series"])} точок ряду') except Exception as e: log(f'ERROR {type(e).__name__}: {e}') raise

    Тут важливі три дрібниці.

    Перша — flush=True. Без неї рядок зависає в буфері до кінця роботи, а у файл потрапляє з запізненням.

    Друга — raise наприкінці. Завдяки їй скрипт завершується з ненульовим кодом, і система це бачить.

    Третя — мітка часу з часовим поясом. Хвостик +03:00 рятує, коли ти порівнюєш лог із часом на своєму комп’ютері.

    ~/logs/dash.log — як це виглядає
    2026-09-02T07:30:01+03:00 START build_dash.py 2026-09-02T07:30:01+03:00 OK 48 записів, 7 точок ряду 2026-09-02T07:45:01+03:00 START build_dash.py 2026-09-02T07:45:01+03:00 ERROR FileNotFoundError: /home/student/data/responses.csv 2026-09-02T08:00:01+03:00 START build_dash.py 2026-09-02T08:00:02+03:00 OK 48 записів, 7 точок ряду

    З цих шести рядків одразу видно, що сталося. О 7:45 щось прибрало CSV. О 8:00 усе відновилося. Без міток часу було б видно тільки «іноді ERROR».

    Що писати і чого не писати

    ПисатиНе писати ніколи
    час початку і час завершенняпаролі, токени, ключі доступу
    скільки записів прочитано й скільки відкинутосамі відповіді однокласників
    куди саме записано результат — повний шляхімена, класи, будь-які персональні дані
    тип помилки й текст виняткуувесь вміст CSV «про всяк випадок»
    скільки секунд тривала роботавміст запитів із чужими даними
    Лог живе довше за все інше Файл із відповідями ти колись почистиш, базу перезбереш, а лог лежатиме місяцями, і ніхто в нього не зазирне. Якщо туди потрапило ім’я однокласника разом із його відповіддю — це витік, навіть якщо лог лежить у приватній теці. Логуй кількість, а не вміст.
    Лог не місце в ~/www/ ~/logs/ — приватна тека, як ~/data/. Лог у публічній теці означає, що будь-хто в інтернеті прочитає, коли твій скрипт падав і які шляхи є в тебе на сервері.

    Ротація: чому лог не може рости вічно

    96 запусків на добу × 2 рядки × 70 байтів — це близько 4,9 МБ на рік. Небагато.

    Але коли скрипт падає, Python друкує в лог не один рядок, а цілу «драбинку». У ній видно, у якому файлі й рядку сталася біда і через які функції код туди дійшов. Ця драбинка зветься traceback. Одна така займає десять-двадцять рядків.

    Кілька сотень падінь — і файл виростає до десятків мегабайтів. Редактор такий файл уже не відкриє, а tail працюватиме повільно.

    Ротація — це коли лог розрізають на частини, а старі частини прибирають.

    Найпростіша ротація для учня — писати одразу в помісячний файл. Пам’ятай про екранування відсотків у crontab:

    помісячний лог + прибирання старого
    */15 * * * * /usr/bin/python3 /home/student/bin/build_dash.py >> /home/student/logs/dash-$(date +\%Y-\%m).log 2>&1 0 4 * * 0 /usr/bin/find /home/student/logs -name 'dash-*.log' -mtime +90 -delete

    Другий рядок раз на тиждень видаляє логи, яких не чіпали понад 90 днів. У «дорослих» системах те саме робить служба logrotate. Вона стискає старі файли й тримає задану кількість поколінь.

    Половина файлу і другий запуск поспіль

    Сторінка дашборда може прийти по data.json рівно в ту мить, коли скрипт його переписує. І отримати половину файлу.

    Лікується це одним прийомом. Пиши в тимчасовий файл, а потім перейменовуй його. У межах однієї файлової системи перейменування миттєве — його називають атомарним. Тому в будь-який момент існує або старий файл цілком, або новий цілком.

    атомарний запис
    import json, os def write_atomic(path, data): tmp = str(path) + '.tmp' with open(tmp, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp, path)

    Ідемпотентність — це коли повторний запуск не псує результат. Наш скрипт ідемпотентний: він щоразу перераховує метрики з нуля й перезаписує файл.

    А якби він дописував точки в series? Два запуски підряд подвоїли б дані. І графік почав би брехати.

    Питання

    У лозі одного учня рядки такі: 2026-09-02T08:00:01 обробляю: Марія Коваленко, 9-Б, «іноді». І так по одному на кожного респондента. Лог лежить у приватній ~/logs/. Що з цим не так?

    Питання

    Учень поставив у crontab > /home/student/logs/dash.log з однією рискою. Скрипт працює вже тиждень. Що він побачить у лозі?

    Крок 7

    Свіжість: наскільки старі твої числа

    Застарілий дашборд гірший за відсутній — бо йому вірять

    Подивись на пакет молока. Там завжди стоїть дата — коли розлито і до якого числа воно придатне. Без цієї дати ти не знаєш, пити його чи вилити.

    З даними так само. Якщо дашборд не відкривається, людина йде шукати їх деінде. А якщо відкривається і виглядає бездоганно? Тоді вона ухвалює рішення за цифрами тижневої давнини. І навіть не здогадується, що щось не так.

    Відсутність даних чесна. Стара цифра без попередження — ні.

    Відповідь на це — одне поле у файлі. Зветься воно generated_at. Скрипт ставить його в момент, коли порахував метрики. Сторінка порівнює це поле з поточним часом і показує різницю.

    Це єдина цифра на дашборді, яку не можна ані підробити випадково, ані «забути оновити».

    ~/www/dash/data.json — початок
    { "generated_at": "2026-09-02T07:30:04+03:00", "source": { "file": "responses.csv", "rows_read": 51, "rows_used": 48 }, "metrics": { "responses_total": 48, "avg_minutes": 37.4 }, "series": [ { "date": "2026-08-27", "responses": 5 } ] }
    Час без часового поясу — половина мітки Якщо записати "2026-09-02T07:30:04", браузер прочитає це як місцевий час читача. Учень із іншого поясу побачить «оновлено 3 години тому» на цілком свіжих даних. Тому в Python пишуть datetime.now().astimezone().isoformat(timespec='seconds') — зсув +03:00 потрапляє у файл сам.

    Звідки сторінка бере ці числа

    Файл data.json лежить поруч зі сторінкою, у теці ~/www/dash/. Але сам собою він у сторінку не потрапить. HTML не вміє читати файли. Це вміє тільки JavaScript.

    Уяви відвідувача бібліотеки. Він підходить до віконця, називає назву книжки й чекає. Через хвилину йому виносять. Та сама дія в браузері зветься fetch — з англійської «сходи й принеси».

    Чекати доводиться завжди: файл лежить на сервері, а не в пам'яті браузера. Тому код пишуть у два кроки. Спершу попросили, потім дочекалися.

    ~/www/dash/index.html — читаємо data.json
    async function load(){ const res = await fetch('data.json?v=' + Date.now()); // сходи й принеси const d = await res.json(); // текст → об'єкт draw(d); // малюємо, коли дані вже є } load();

    Розберемо ці рядки. fetch('data.json') просить у сервера файл за його адресою. res.json() перетворює отриманий текст на об'єкт, у якого вже можна питати d.metrics.responses_total.

    Слово await означає «зачекай тут, поки не принесуть». Без нього код побіжить далі з порожніми руками й намалює порожній дашборд.

    Хвостик ?v= із числом — прийом проти кешу. Браузер любить показувати вчорашню копію файлу, якщо адреса та сама. Змінне число робить адресу щоразу новою, і браузер таки йде на сервер.

    Числа в HTML не зашивають Спокуса написати <b>48</b> прямо в розмітці велика: так швидше й одразу видно результат. Але вже наступного дня сторінка починає брехати, а cron працює намарно. Усі чотири картки й обидва графіки беруть числа з data.json — і тільки звідти.

    Плашка на сторінці

    Плашка рахує вік від generated_at, а не від моменту, коли сторінка відкрилася. Це різні речі. Сторінку можна відкрити о будь-якій годині — дані від цього свіжішими не стануть.

    ~/www/dash/index.html — фрагмент
    const age = (Date.now() - Date.parse(d.generated_at)) / 60000; // хвилин badge.textContent = 'оновлено ' + fmtAge(age); badge.className = 'badge ' + (age <= 20 ? 'g' : age <= 30 ? 'y' : 'r');
    оновлено 6 хв тому оновлено 24 хв тому дані застаріли: 3 дні усе за розкладом один запуск пропущено автоматика не працює свіжі увага застарілі 0 10 20 30 50 60+ хв Пороги рахуються від розкладу, а не з голови: при запуску раз на 15 хвилин 20 хвилин — це «один запуск затримався», 30 — «двох запусків не було». При щоденному розкладі ті самі 30 хвилин означали б, що все чудово.
    Пороги завжди прив’язують до власного розкладу. Для дашборда цього уроку (раз на 15 хвилин) межі 20 і 30 хвилин; для щоденного звіту зеленою була б доба, а червоним — три дні.

    Як це виглядає на сторінці

    відповідей усього48
    середній час, хв37,4
    медіана, хв35
    частка понад 45 хв29%

    оновлено 6 хвилин тому

    Чотири картки, під ними плашка. Числа тут ілюстративні — у тебе будуть свої, з data.json.

    Два графіки: Chart.js

    Чотири картки показують стан на зараз. Але «48 відповідей» нічого не каже про те, росте це чи падає. Тому поруч ставлять графік: він показує ту саму метрику в часі.

    Креслити осі, сітку й підписи руками довго. Готова бібліотека, яка робить це за тебе, зветься Chart.js. Ти даєш їй список підписів і список чисел — вона малює лінію, підписує осі й показує значення, коли навести курсор.

    Бібліотека — це один файл із чужим кодом. Він уже лежить на сервері, у /opt/school/vendor/chartjs/: качати нічого не треба, ти просто скопіюєш його до себе в ~/www/dash/assets/. Команду дає крок практики.

    Місце під сам графік розмічають тегом <canvas>. Це порожній прямокутник, усередині якого JavaScript має право малювати.

    ~/www/dash/index.html — місце під графіки
    <canvas id="cSeries" height="180"></canvas> <canvas id="cTools" height="180"></canvas> <script src="assets/chart.min.js"></script>

    Тепер сама діаграма. Функція draw(d) — та сама, яку викликав load() після fetch.

    ~/www/dash/index.html — перший графік
    function draw(d){ new Chart(document.getElementById('cSeries'), { type: 'line', // лінія по днях data: { labels: d.series.map(p => p.date), datasets: [{ label: 'відповідей', data: d.series.map(p => p.responses) }] } }); }

    Читається це так. type — яким графіком малювати: 'line' для змін у часі, 'bar' для порівняння величин між собою.

    labels — підписи по горизонталі, у нас це дати. datasets — самі числа, які лягають на графік.

    .map() проходить по масиву series з твого data.json і витягає з кожної точки одне поле. Із записів виду {"date":"2026-08-27","responses":5} виходить окремо список дат і окремо список чисел. Рівно те, чого чекає Chart.js.

    Другий <canvas> роби стовпчиками: type: 'bar' по інструментах. У labels підуть їхні назви, у data — скільки разів кожен назвали. Обидва графіки читають той самий data.json, який щочверть години перезаписує cron.

    Бібліотеку бери свою, а не з чужого сайту Рядок <script src="https://…"> працює тільки доти, доки чужий сайт живий і доки в класі є інтернет. На очній здачі це підводить найчастіше. Свій chart.min.js — звичайний текст, і сервер приймає його по FTP без заперечень.
    «Дашборд-детектив»: 12 віджетів → 4 Дашборд читають три секунди. Якщо на ньому дванадцять чисел, читач не прочитає жодного. Залиш ті чотири KPI, які відповідають на твої чотири питання з минулого уроку. Решту прибери: це не «додаткова інформація», а шум, який ховає головне.
    Питання

    Плашка на дашборді бадьоро повідомляє «оновлено 2 хвилини тому». А ти щойно бачив, що data.json не змінювався три дні. Відкриваєш код і бачиш const t0 = Date.now() на початку скрипта. Що відбувається?

    Питання

    Твій скрипт зламався в п’ятницю ввечері, ти помітив це в понеділок. Дашборд усі вихідні показував п’ятничні числа й виглядав ідеально. Що зробити в першу чергу — ще до того, як лагодити скрипт?

    Крок 8

    Симулятор доби

    Одна модельна доба: коли спрацьовує скрипт, що з’являється в лозі та якого кольору стає плашка

    Перемикач «зламати» не пише розпливчасте «щось не спрацювало». Він показує точні рядки, які ти побачиш у своєму ~/logs/dash.log.

    Найважливіший перемикач — 2>&1. Вимкни його й подивись, як лог стає порожнім саме тоді, коли все ламається.

    Модельна доба
    розклад у crontab
    що зламано
    пороги плашки свіжості
    10:00
    запусків від 00:000
    успішних0
    рядків у лозі0
    вік даних

    Числа в рядках лога («48 записів», «0.31 c») ілюстративні — вони показують форму запису, а не твої дані.

    Питання

    У симуляторі вибери «відносний шлях у скрипті». Подивись на лог і на плашку одночасно. Яка комбінація там виходить і чому вона найнебезпечніша?

    Крок 9

    За розкладом нічого не сталося. Що робити

    Чотири вузли ланцюжка — і по одній команді на кожен

    Ланцюжок від cron до сторінки короткий. Зламатися він може рівно в чотирьох місцях. Проходь їх по черзі зліва направо й не перескакуй. 80% часу причина знаходиться на першому або другому кроці.

    cron скрипт data.json сторінка розклад і запуск build_dash.py ~/www/dash/ fetch + Chart.js запису немає взагалі не той акаунт п’ять полів написані не в тому порядку служба cron не працює PATH: команду не знайдено відносний шлях немає прав на запис падає на порожньому CSV вивід нікуди не йде файл є, але старий ліг не в ту теку зіпсований на пів запису права 600 — nginx не віддасть generated_at без поясу старий файл із кешу fetch мовчки впав плашка рахує від Date.now() графік малює зашиті числа помилка в консолі браузера перевір: crontab -l whoami перевір: tail -n 30 dash.log env -i /bin/sh -c '…' перевір: ls -l data.json head -c 200 data.json перевір: DevTools → Network DevTools → Console Іди зліва направо. Найчастіша причина — другий вузол: cron дав скрипту інше оточення, ніж твоя SSH-сесія. Найпідступніша — третій: скрипт «успішно» записав файл не туди.
    Друга смуга — команда, якою вузол перевіряють за десять секунд. Ніякого вгадування: кожен крок або підтверджує, або відкидає одну гіпотезу.

    Крок 1. Чи запис узагалі є

    сервер, ssh
    $ whoami student $ crontab -l no crontab for student

    Такий вивід закриває питання одразу: розкладу немає. Найчастіша причина проста. Редактор запитав «зберегти?», а учень натиснув «ні». Друга причина — crontab -e виконали від іншого користувача.

    Крок 2. Що каже лог

    сервер, ssh
    $ tail -n 30 ~/logs/dash.log $ ls -l ~/logs/dash.log -rw-r--r-- 1 student student 0 вер 2 07:15 /home/student/logs/dash.log

    Файл є, розмір нуль. Отже, cron рядок виконав — інакше файл би не створився. Але вивід у лог не потрапив. Перше, що перевіряєш, — чи є в рядку 2>&1.

    Крок 3. Та сама команда, те саме оточення

    Запускати руками в своїй сесії безглуздо: там усе працює, ти це вже знаєш. Треба відтворити бідне оточення cron. Команда env -i запускає програму з порожнім набором змінних:

    відтворюємо оточення cron
    $ cd ~ && env -i /bin/sh -c 'build_dash.py' /bin/sh: 1: build_dash.py: not found $ cd ~ && env -i /bin/sh -c '/usr/bin/python3 /home/student/bin/build_dash.py' 2026-09-02T10:14:03+03:00 START build_dash.py 2026-09-02T10:14:03+03:00 OK 48 записів, 7 точок ряду

    Різниця між двома рядками — і є діагноз. Перший показує рівно те, що бачив cron. Другий доводить, що з абсолютними шляхами все працює.

    Крок 4. Системний журнал, якщо він доступний

    Сам cron теж пише про кожен запуск. Але пише він у системний журнал, а не до тебе в теку. На частині серверів звичайний користувач туди не має доступу, і це нормально:

    якщо доступ є
    $ grep CRON /var/log/syslog | tail -n 5 Sep 2 10:15:01 vps CRON[20481]: (student) CMD (/usr/bin/python3 /home/student/bin/build_dash.py)

    Такий рядок доводить головне: cron команду запустив. Якщо рядок є, а результату немає — проблема всередині скрипта. Тоді шукай у своєму лозі, а не в розкладі.

    Питання

    У /var/log/syslog видно рядки CRON … CMD (…build_dash.py) кожні 15 хвилин. У ~/logs/dash.log — жодного рядка за сьогодні, файл не порожній, але останній запис учорашній. Про що це говорить?

    Крок 10

    Практика: розклад, лог, сторінка

    Кроки 1—11 — у SSH-сесії на сервері, кроки 12—13 — у твоєму редакторі, крок 14 — по FTP, кроки 15—16 — знову на сервері. Галочки зберігаються, сторінку можна закрити.

    Скрізь підставляй свій логін У командах написано student — це не твій логін. Дізнайся свій через whoami і заміни в усіх абсолютних шляхах. Скопійований без правки рядок або не працюватиме, або (гірше) писатиме в чужу теку.
    Крок 11

    Автоперевірка

    Головний тест не читає твій код. Він видаляє data.json і чекає, чи повернеться файл сам.

    Демонстраційний режим: результат згенеровано для показу. На сервері ця кнопка викликає POST /api/check, який запускає check 12 від імені учня. Тест самовідновлення триває до 16 хвилин — запускай його не в останню хвилину уроку.

    Як саме працює тест самовідновлення Сервер переносить твій ~/www/dash/data.json у резервну копію й засікає час. Далі він раз на хвилину перевіряє, чи файл з’явився. Якщо за 16 хвилин файл повернувся і generated_at у ньому не старший за 20 хвилин — критерій зараховано. Створити файл руками в цей момент не вийде: сервер бачить, о котрій він з’явився і чи збігається час усередині.

    Що вчитель дивиться на твоєму екрані

    Крок 12

    Шкала виконання

    Це те, що бачить учитель, коли виставляє оцінку.

    СкладникВагаРезультат
    Виконано
    0%
    Рекомендований бал за 12-бальною

    Чотири речення, які варто запам’ятати

    Де найчастіше помиляються

    Домашнє завдання

    Додай у build_dash.py обробку помилки. Обгорни читання CSV у try/except. Запиши в лог рядок ERROR з типом винятку. І перевір, що дашборд при цьому не ламається. Для перевірки тимчасово зроби порожній CSV:

    перевірка обробки помилки
    $ cp ~/data/responses.csv ~/data/responses.bak $ : > ~/data/responses.csv $ /usr/bin/python3 ~/bin/build_dash.py; echo "код виходу: $?" $ cp ~/data/responses.bak ~/data/responses.csv

    Правильна поведінка виглядає так. У лозі з’являється ERROR. Старий data.json залишається недоторканим, а не перетворюється на порожній. А плашка на сторінці поступово жовтіє й червоніє.

    Опиши цю поведінку трьома реченнями в ~/www/dash/README.md.

    Джерела

    Звідки взяті факти

    Синтаксис cron, поведінка оточення й формат мітки часу — з документації та стандартів. Перевіряти дозволено й корисно.

    Повний список із поясненнями, що звідки взято, — у файлі urok-12-джерела.md поруч із цією сторінкою.