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

Звіт сам себе пише: шаблон, скрипт, cron

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

Учора ти зібрав відповіді. Сьогодні сторінка рахуватиме їх сама

Тема 7, урок 2 з 2. Артефакти: bin/report.tpl.html, bin/report.sh і згенерований report.html

На минулому уроці форма на твоєму сайті почала писати відповіді в ~/data/responses.csv. Користі з цього файлу поки мало. Щоб побачити, що люди думають про продукт, треба зайти по SSH і прочитати файл очима.

Раз на тиждень ти це зробиш. Раз на день — уже ні. А на Demo Day журі спитає: «скільки у вас відгуків і яка середня оцінка». І ти згадуватимеш число з голови.

Сьогодні між приватним CSV і публічною сторінкою стане конвеєр. Це маленький скрипт, який робить чотири дії підряд. Читає файл із відповідями. Рахує кілька чисел. Підставляє їх у заготовку сторінки. І кладе готовий report.html у ~/www/.

А запускати цей скрипт за розкладом буде cron — вбудований у сервер будильник. Ти ставиш час один раз, далі він працює сам. З цієї хвилини сторінка звіту оновлюється без тебе.

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

Хід уроку

ЕтапХвЩо робимо
Вхідний зріз10Тестовий POST по класу — латаємо учорашній обробник
Mail merge15Шаблон із «дірками» — розмічаємо report.tpl.html плейсхолдерами
report.sh30Середній рейтинг, розподіл оцінок, ТОП-3 — перевіряємо числа вручну на 3 рядках
Тригери й cron15«За подією» проти «за розкладом» — складаємо свій cron-вираз
Практика: розклад15crontab -e, перенаправлення виводу в лог, перевірка mtime
«Зламай конвеєр»20Читаємо реальні помилки, локалізуємо зламаний вузол, лагодимо
Автоперевірка й підсумок15check11 7 повністю, звіряємо числа звіту з еталоном
Разом120

Слова шаблон, cron і атомарний запис поки нічого не означають — розберемо кожне тоді, коли воно знадобиться.

Головне обмеження уроку ~/data/responses.csv містить імена й коментарі реальних однокласників. У публічний report.html іде тільки агрегат: кількість відгуків, середня оцінка, розподіл по категоріях. Жодного імені, жодного коментаря, жодного часу конкретної відповіді. Це наскрізне правило курсу, і сьогодні воно стає окремим критерієм автоперевірки — no_pii.
ФайлХто його створюєДе лежитьПублічний?
data/responses.csvобробник форми (урок 11)~/data/ні — поза вебкоренем
bin/report.tpl.htmlти, руками, один раз~/www/bin/так, але це порожній бланк
bin/report.shти, руками, один раз~/www/bin/так, але виконує його cron, не браузер
report.htmlскрипт, щогодини~/www/так — це і є звіт
logs/report.logcron, при кожному запуску~/logs/ні, і ніколи не має бути

Зверни увагу на четвертий рядок: report.html — єдиний файл проєкту, який ти не пишеш руками. Ти пишеш те, з чого його роблять. Якщо колись відкриєш його в редакторі й щось поправиш — наступний запуск cron мовчки затре твою правку. Це не поламка, це те, як живуть згенеровані файли: правити треба джерело.

Що з цього робиться в терміналі, а що по FTP Файли report.tpl.html і report.sh ти пишеш у себе на компʼютері в редакторі. Заливаєш їх по FTP у ~/www/bin/. Дві речі FTP-клієнт зробити не вміє. Це право на запуск (chmod +x) і рядок розкладу в crontab — їх ставиш по SSH. А report.html по FTP не заливається взагалі. Перевірка дивиться, звідки він узявся: якщо його залили, а не згенерував скрипт, вона попросить переробити.
Крок 2

Шаблон окремо, дані окремо

Той самий прийом, що й у злитті листів: бланк із «дірками» плюс таблиця значень

Ідея стара, як друкарський верстат. Є бланк, у якому все постійне вже надруковано, а на місці змінного лишені порожні місця. Є дані — таблиця, де кожен рядок заповнює ці місця по-своєму. Механізм, що складає одне з другим, називають mail merge, злиттям або шаблонізацією — суть однакова.

У нашому випадку бланк — це report.tpl.html. Це справжня HTML-сторінка: з версткою, підключеним brand.css і заголовками.

Замість чисел у ній стоять помітні маркери на кшталт {{COUNT}}. Такий маркер зветься плейсхолдер — місце, куди пізніше стане значення. Самих даних у файлі немає жодних.

report.tpl.html — бланк <h1>Що кажуть люди</h1> {{COUNT}} відгуків середня {{AVG}} з 5 оновлено {{UPDATED}} пишеш руками · один раз ~/data/responses.csv — дані ts,name,rating,feature,comment …,Коваль О.,5,експорт,«зручно» …,Марчук І.,4,темна тема,«ок» приватне · пише обробник форми report.sh читає обидва report.html — результат <h1>Що кажуть люди</h1> 14 відгуків середня 4.07 з 5 оновлено 15.09 21:05 перезаписується щогодини руками не редагується імен і коментарів усередині немає: вони не перетнули межу публікації
Три файли, три різні життєві цикли. Бланк і дані — джерела, їх скрипт тільки читає. Результат одноразовий: його можна видалити будь-коли, бо наступний запуск зробить його заново. Числа на схемі — приклад.

Чому логіку не змішують із розміткою

Спокуса очевидна: навіщо два файли, якщо можна навчити скрипт друкувати HTML прямо в себе — echo "<p>$N відгуків</p>" >> report.html? Перші пʼять рядків справді пишуться швидше. Далі починається розплата.

ЗадачаШаблон окремоHTML усередині скрипта
Змінити колір заголовкаправиш brand.css або шаблон, скрипта не чіпаєшлізеш у скрипт із set -e й лапками
Додати абзац текступишеш звичайний HTML у редакторі з підсвіткоюекрануєш лапки й $, ловиш зламані рядки
Побачити, як виглядатимевідкриваєш шаблон у браузерітільки після запуску скрипта
Дати верстку іншій людинівіддаєш один HTML-файлвіддаєш свій код і пояснюєш його
Зламати верстку помилкою в логіцінеможливо: логіка не бачить розміткилегко: одна лапка — і сторінки немає

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

Так само влаштовано багато чого навколо. Вихідний код і зібрана програма. Файл .md і згенерований із нього .html з уроку 8. Фотографія й маленьке превʼю до неї.

Як має виглядати плейсхолдер

Маркер має бути таким, щоб його неможливо було зустріти у звичайному тексті сторінки. Слово «відгуків» у шаблоні трапиться разів десять, а {{COUNT}} — жодного зайвого разу.

Форму «подвійні фігурні дужки плюс великі латинські літери» ти вигадувати не мусиш. Так само позначають дірки в бланку готові програми-заповнювачі. Ти даєш такій програмі шаблон і таблицю значень, а вона віддає готову сторінку. Звуться такі програми шаблонізаторами.

Дві найвідоміші — Jinja і Handlebars. Першою заповнюють сторінки програми, написані на Python, другою — написані на JavaScript. Сьогодні ти не ставитимеш ані ту, ані ту: три заміни в тебе робить sed. Але позначку береш таку саму, щоб потім не переучуватися.

ПлейсхолдерЩо підставляєтьсяПриклад значення
{{COUNT}}скільки всього відгуків14
{{COUNT_WORD}}«відгук» / «відгуки» / «відгуків»відгуків
{{AVG}}середня оцінка, два знаки4.07
{{DIST}}готові <li> розподілу 5★…1★одним рядком
{{TOP}}готові <li> топ-3 функційодним рядком
{{UPDATED}}час запуску для людини15.09.2026 21:05
{{UPDATED_ISO}}той самий час для <time>2026-09-15T21:05:02+03:00
Крапка чи кома в середній оцінці У самому звіті середня стоїть із крапкою: 4.07. Так це число друкує awk: він рахує в локалі C.UTF-8, а там роздільник дробової частини завжди крапка. У тексті цього уроку та сама оцінка написана з комою — 4,07, як прийнято українською. Число одне, запис різний; на підрахунки це не впливає.
Дві пастки в самому шаблоні Перша стосується списків {{DIST}} і {{TOP}}. Вони підставляються одним рядком без переносів. Причина в тому, що sed працює порядково й не вміє вставити багаторядковий текст. Друга — про вигляд. Відкриєш report.tpl.html у браузері й побачиш {{COUNT}} відгуків. Це нормально: шаблон не є звітом і не має ним виглядати.
Питання

Ти вирішив обійтися без шаблону: хай скрипт відкриває сам report.html і замінює в ньому старе число на нове. Перший запуск пройшов, другий теж, а після третього у звіті замість кількості відгуків стоїть 4.07 двічі підряд і поїхала верстка. Чому такий підхід приречений із самого початку?

Крок 3

Межа: що переходить у публічний звіт, а що ні

Конвеєр перетинає цю межу один раз, і що саме через неї пройде — вирішуєш ти

Твій конвеєр складається з чотирьох ланок: приватний CSV → скрипт → шаблон → публічний report.html. Три перші ланки живуть поза вебкоренем або містять тільки бланк. Четверта відкрита кожному, хто знає адресу. Між третьою і четвертою проходить межа публікації. Рішення, що саме її перетне, ухвалює одна людина. Це ти, і робиш ти це тоді, коли пишеш скрипт.

ПРИВАТНА ЗОНА · ~/data/ · по HTTP не віддається ПУБЛІЧНА ЗОНА · ~/www/ · бачить кожен responses.csv · 14 рядків ts · коли саме відповіли name · імʼя людини rating · оцінка 1–5 feature · яку функцію просять comment · вільний текст МЕЖА ПУБЛІКАЦІЇ report.sh агрегує report.html 14 — скільки відгуків 4.07 — середня оцінка 5★:5 4★:6 3★:2 2★:1 1★:0 топ-3: експорт, тема, нагадування жодного рядка з людиною всередині кожне число тут — сума багатьох відповідей, а не чиясь одна відповідь, переписана словами
Дві колонки з пʼяти дають усі числа звіту. Три інші — ts, name, comment — зупиняються перед межею. Не тому, що вони не потрібні: comment — найцінніше, що в тебе є, але читаєш його ти, а не інтернет. Числа на схемі — приклад.

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

Фізична. ~/data/ не віддається по HTTP. Це не налаштування сторінки й не «ніхто не знає адреси» — вебсервер просто не бачить цієї теки. Тому файл із людьми лежить там і нікуди звідти не копіюється — навіть тимчасово, навіть «щоб подивитися в браузері».

Логічна. Скрипт читає пʼять колонок, а записує тільки похідні від двох. Похідне число — це сума багатьох відповідей. «14 відгуків» не вказує ні на кого; «Марчук І. поставив 2» вказує на конкретну людину, яку завтра спитають, чому вона так зробила.

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

Логи — теж дані Хочеться покласти report.log у ~/www/, щоб дивитися його з телефона. Не роби так. У лог часто потрапляють шматки рядків із CSV — вони приїжджають туди разом із повідомленнями про помилки. Лог живе в ~/logs/ і читається по SSH.
Питання

У твоєму report.tpl.html зʼявився рядок {{COMMENTS}}, і скрипт підставляє туди все, що люди написали, разом з іменами. Файл лежить у ~/www/. Посилання на нього ніде немає. Що станеться найгірше?

Питання

Учитель просить показати, звідки в звіті взялося число 14. Однокласник радить: «поклади responses.csv у ~/www/data/ поруч із market.csv — буде прозоро, кожен перевірить». Що відповіси?

Крок 4

Агрегат: чому з числа не повернутися до рядків

Число, пораховане з багатьох рядків, не розкладається назад на рядки

Слово «агрегат» звучить бюрократично, а означає просту річ: ти взяв багато окремих записів і замінив їх кількома числами. Кількість, сума, середнє, розподіл, топ — усе це агрегати. Головна їхня властивість — незворотність. З 14 рядків ти зробиш число 4,07 за секунду. З числа 4,07 ти не зробиш назад жодного рядка.

сирі рядки · те, що читаєш тільки ти 09:40 Марчук І. 4 темна тема «поки ок» 09:52 Коваль О. 3 експорт «довго шукав» 11:03 Гнатюк С. 5 експорт «супер» 13:31 Лисюк А. 2 мобільна «не працює» 14:05 Дідух М. 4 нагадування «треба» 5 людей · 5 думок · 5 моментів часу агрегація секунда роботи назад — ніяк агрегат · те, що бачить інтернет 5 відгуків середня 3.60 з 5 5★:1 4★:2 3★:1 2★:1 1★:0 топ: експорт 2, темна тема 1 4 числа замість 5 історій Що з правої картки вже не відновити · хто саме поставив 2★ — і чи не образився він · що людина написала своїми словами — сама причина оцінки · коли вона відповіла — до чи після того, як ти полагодив мобільну версію · чи відповідала ця сама людина минулого тижня · скільки людей узагалі бачило форму й не відповіли · чи всі пʼятеро з одного класу — тобто наскільки це «ринок»
Незворотність — це водночас захист і втрата. Захист: із правої картки нікого не витягнеш. Втрата: правий бік не відповідає на питання «чому». Тому сирі дані ти зберігаєш і читаєш сам — просто не публікуєш. Дані на схемі вигадані для прикладу.

Коли агрегат перестає захищати

Чесно про межу цього прийому: агрегат ховає людину, поки людей багато. Уяви, що відгуків два. Ти показуєш розподіл «5★ — 1, 2★ — 1» і поруч пишеш «оновлено 14:05». А в класі всі бачили, хто заповнював форму о 14:00.

Така «анонімна статистика» вказує пальцем не гірше за імʼя. Це називають деанонімізацією через малу вибірку.

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

Формально Європейський регламент захисту даних (GDPR) не поширюється на «анонімну інформацію». Так він називає дані, за якими людину не можна впізнати жодними доступними засобами (Recital 26). Ключові слова тут — не можна впізнати. Якщо клас вирахує автора з твоїх чотирьох чисел за пʼять секунд, статистика не анонімна. Як би вона не називалася.
Питання

У твоєму звіті: 14 відгуків, середня 4,07, розподіл 5★—5, 4★—6, 3★—2, 2★—1, 1★—0. Однокласник каже: «за цими числами я відновлю, хто що поставив». Він має рацію?

Крок 5

Конструктор звіту

Обери, що показує твій звіт, — сторінка збере під це шаблон і скрипт

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

Внизу зʼявиться готовий report.tpl.html із плейсхолдерами й report.sh, який їх заповнює. Обидва можна скопіювати й доробити під свій продукт.

Конструктор звіту

Які агрегати показувати

Що побачить відвідувач

Файл 1 — шаблон

~/www/bin/report.tpl.html

    

Файл 2 — скрипт, що його заповнює

~/www/bin/report.sh

    

Логін <логін> у шляхах заміни на свій — інакше скрипт читатиме чужу теку й отримає «Permission denied». Перевір себе командою whoami.

Питання

Ти вмикаєш у конструкторі «динаміку за 7 днів» і «топ-3 функції». Відгуків поки 6. Що з цим не так?

Крок 6

Як скрипт читає CSV і рахує числа

Дві короткі команди читають responses.csv і дістають із нього кількість, середню оцінку й топ-3 функції

CSV із уроку 11 має пʼять колонок: ts,name,rating,feature,comment. Перший рядок — заголовок, його треба пропустити. Далі кожен рядок — одна відповідь, і нас цікавлять лише третя й четверта колонки.

Чотирнадцять рядків можна скласти й очима. Сто сорок — уже ні: зібʼєшся на третьому десятку. Потрібен помічник, який пробіжить файл згори вниз і складе числа сам.

Такий помічник у Linux уже стоїть і зветься awk. Йому кажуть одне речення: «бери кожен рядок, дивись на третю колонку, додавай її до суми». Він читає файл рядок за рядком і робить рівно це — і нічого більше.

Речення для awk пишуть у лапках, а складається воно з двох частин. Спершу умова: для яких рядків працювати. Потім у фігурних дужках — що з такими рядками робити. Ключ -F',' попереду каже, що колонки у файлі розділені комою.

кількість і сума оцінок
awk -F',' 'NR>1 && $3+0>=1 && $3+0<=5 { n++; s += $3 }
           END { print n+0, s+0 }' ~/data/responses.csv

NR>1 пропускає заголовок. $3+0 примусово перетворює текст на число. Якщо в клітинці «пʼять» або порожньо, вийде 0, і умова >=1 відсіє такий рядок.

n+0 у кінці теж потрібне. Воно друкує 0 на порожньому файлі замість порожнього місця. Інакше наступний крок отримає порожній рядок замість числа.

Чому простий -F',' тут безпечний Кома всередині коментаря справді додасть зайві поля — але тільки після пʼятої колонки. $3 і $4 стоять раніше, тож не зсуваються. А от якби comment був другою колонкою, весь підрахунок посипався б. Порядок колонок у CSV — це домовленість між обробником форми і звітом; міняєш одну сторону — міняй і другу.

Розподіл оцінок і топ-3 функції

розподіл 5★…1★ одним рядком
awk -F',' 'NR>1 { r = $3+0; if (r>=1 && r<=5) c[r]++ }
           END { for (i=5; i>=1; i--) printf "<li>%d★ — %d</li>", i, c[i]+0 }' \
    ~/data/responses.csv
топ-3 функції з нормалізацією регістру
awk -F',' 'NR>1 { f = tolower($4); gsub(/^[ \t]+|[ \t]+$/, "", f);
                  if (f != "") c[f]++ }
           END { for (k in c) printf "%d\t%s\n", c[k], k }' ~/data/responses.csv \
  | sort -rn -k1,1 | head -3 \
  | awk -F'\t' '{ printf "<li>%s — %d</li>", $2, $1 }'

Два прийоми тут варті окремої уваги. tolower плюс gsub прибирають найпоширенішу помилку звітів. «Експорт», «експорт» і «експорт » потрапляють в один кошик, а не в три.

Другий прийом — printf без \n у кінці. Списки через нього складаються одним довгим рядком. Саме такий вигляд потрібен для підстановки через sed.

tolower і кирилиця: перевір це на своєму сервері tolower опускає регістр за правилами поточної локалі. У твоїй SSH-сесії локаль зазвичай UTF-8, і «Експорт» справді стає «експорт». А cron стартує задачу без твого оточення — у локалі C, де для awk існують тільки латинські літери. Тоді «Експорт» і «експорт» знову розʼїжджаються по різних кошиках, і топ-3 у звіті виглядає інакше, ніж при ручному запуску. Лікується одним рядком на початку скрипта: export LC_ALL=C.UTF-8. Перевірити різницю можна прямо зараз:
та сама команда у двох локалях
$ LC_ALL=C awk 'BEGIN{print tolower("Експорт у CSV")}' Експорт у csv $ LC_ALL=C.UTF-8 awk 'BEGIN{print tolower("Експорт у CSV")}' експорт у csv

Підстановка в шаблон

Числа пораховані, шаблон лежить поруч. Лишилося замінити в шаблоні {{COUNT}} на 14, а {{AVG}} — на середню оцінку. Руками цього не зробиш: скрипт запускається щогодини, і ніхто не сидітиме біля нього з мишкою.

Заміну тексту робить команда sed. Вона теж читає файл рядок за рядком, але не рахує, а міняє одне на інше по дорозі. Сам файл sed не чіпає: змінений текст він віддає далі, а куди його покласти — вирішуєш ти.

Запис s|що шукати|на що поставити|g читається просто. s — це «замінити», від англійського substitute. Риски відділяють одне від одного. g у кінці означає «в усьому рядку, а не тільки в першому місці».

sed із безпечним роздільником
sed -e "s|{{COUNT}}|$N|g" \
    -e "s|{{AVG}}|$AVG|g" \
    -e "s|{{DIST}}|$DIST|g" \
    "$TPL" > "$TMP"
Три символи, що ламають sed Скісна /. Уяви значення <li>експорт у PDF/CSV — 5</li>: скісна в ньому закрила б вираз посеред рядка. Тому роздільником беруть |, а не звичну /. Амперсанд & у правій частині означає «весь знайдений збіг» — у тексті його треба екранувати. Перенос рядка в значенні не підставляється взагалі: sed працює порядково. Саме тому всі списки збираються одним рядком.
Питання

Скрипт падає з повідомленням sed: -e expression #1, char 41: unknown option to `s'. У шаблоні стоїть плейсхолдер {{TOP}}, а в змінній TOP лежить рядок <li>експорт у PDF/CSV — 5</li>. Що сталося?

Крок 7

Атомарний запис: сторінка не має бути порожньою жодної мілісекунди

Різниця між > report.html і «тимчасовий файл плюс mv»

Здається, що записати результат — найпростіший крок конвеєра: sed … > report.html, і все. Проблема в тому, що > робить дві дії, а не одну. Спершу оболонка відкриває файл і спорожняє його — файл стає нульового розміру. Лише потім у нього починає текти вміст. Між цими двома моментами минають мілісекунди, і всі вони — реальний час, у який файл існує й він порожній.

Девʼятсот девʼяносто девʼять разів із тисячі цього ніхто не побачить. Тисячний раз трапиться тоді, коли сторінку відкриє журі на Demo Day.

ПРЯМО: sed … > report.html t₁ відкриття t₂ запис триває t₃ готово 0 байтів половина файлу повна сторінка відвідувач у цю мить бачить білий екран або обрізаний HTML ЧЕРЕЗ ТИМЧАСОВИЙ ФАЙЛ: sed … > .report.XXXXXX ; mv .report.XXXXXX report.html t₁ mktemp t₂ запис триває t₃ mv — одна дія .report.aB3x — 0 б .report.aB3x пишеться імʼя переставлено report.html: стара report.html: стара report.html: нова порожнього стану немає взагалі: у кожен момент часу за іменем report.html лежить цілий файл
Верхня смуга — те, що бачить відвідувач при прямому записі: два стани з трьох зламані. Нижня — при записі через тимчасовий файл: зламаних станів немає, лише «стара» і «нова».

Чому mv — це одна дія

Під mv усередині одного диска ховається системний виклик rename(). Його документація обіцяє прямо: якщо файл із новим імʼям уже існує, він замінюється миттєво. Немає такої частки секунди, у яку хтось побачив би, що файлу немає.

Причина проста. Ядро переставляє один запис у каталозі, а вміст файлів нікуди не копіюється. Тому час операції не залежить від розміру файлу.

Одна умова, без якої гарантія зникає rename() працює тільки в межах однієї файлової системи. Припустимо, ти пишеш mv /tmp/report.html ~/www/report.html, а /tmp лежить на іншому диску. Тоді mv мовчки перейде на «скопіювати, потім видалити». І повернеться той самий порожній стан посередині. Саме тому тимчасовий файл створюють у теці призначення: mktemp "$(dirname "$OUT")/.report.XXXXXX". Крапка на початку імені ховає його від ls і від індексного списку вебсервера.

Ще одна дрібниця з наслідками: mktemp створює файл із правами 600 — читати може тільки власник. Після mv ці права дістануться report.html, і вебсервер відповість 403 Forbidden. Тому перед перейменуванням ставлять chmod 644. Помилка непомітна на своєму компʼютері й гарантована на сервері.

Питання

Скрипт пише результат прямо: sed … "$TPL" > "$OUT". Рівно в цю мілісекунду сторінку відкриває людина, якій ти щойно надіслав посилання. Що вона побачить і як цього уникнути?

Крок 8

cron: розклад, абсолютні шляхи, лог

Планувальник, який запускає твій скрипт, поки ти на уроці алгебри

У 9 класі ви вже розбирали планувальники докладно. Тому тут буде тільки те, що потрібно для звіту. Плюс одна річ, на якій спотикаються всі.

Розклад описується пʼятьма полями. Читаються вони зліва направо: хвилина, година, день місяця, місяць, день тижня. Зірочка означає «будь-яке значення».

5 * * * * /bin/bash /home/koval.d.11a/www/bin/report.sh 5** ** хвилинагодина день місяцямісяць день тижня 0–590–23 1–311–12 0–7, 0 і 7 = неділя Як це читається людською мовою 5 * * * * → на пʼятій хвилині кожної години, щодня — 24 запуски на добу 30 7 * * 1-5 → о 07:30 з понеділка по пʼятницю; */15 * * * * → кожні 15 хвилин
Пʼять полів і команда. Скорочення @hourly і @daily — те саме, що 0 * * * * і 0 0 * * *.
Пастка пʼятого поля Якщо обмежені обидва поля дня — і день місяця, і день тижня, — вони працюють через «або», а не «і». Вираз 0 9 13 * 5 спрацює і тринадцятого числа, і кожної пʼятниці. Це задокументована поведінка, а не помилка; на неї натикаються майже всі.

Чому скрипт працює в тебе й падає в cron

Найчастіша скарга уроку звучить так: «у терміналі все працює, а по розкладу — ні». Причина одна: cron не є твоєю сесією. Коли ти заходиш по SSH, оболонка читає ~/.bashrc і ~/.profile, підхоплює твій PATH, псевдоніми, змінні. Cron нічого цього не робить. Він ставить SHELL=/bin/sh. Бере HOME і LOGNAME із системного файлу користувачів. І стартує задачу в домашній теці. Твоїх налаштувань там немає.

Що ти звик матиЩо бачить cronНаслідок
поточна тека — там, де ти бувзавжди домашня текаbin/report.sh шукається як ~/bin/report.sh
PATH із ~/.bashrcкороткий системний PATHcommand not found на власних утилітах
оболонка bash/bin/shпадають [[ ]], масиви, <<<
вивід на екранвивід іде поштою або в нікудипомилку ніхто не бачить місяцями

Із цих чотирьох рядків випливає три висновки. Перший: абсолютні шляхи всюди — і до скрипта в crontab, і до кожного файлу всередині скрипта. Другий: явно назви програму, яка виконає скрипт, — /bin/bash у рядку розкладу, а не надія на #!. Третій: заведи власний лог замість пошти.

рядок у crontab -e
5 * * * * /bin/bash /home/<логін>/www/bin/report.sh >> /home/<логін>/logs/report.log 2>&1

>> дописує в кінець файлу, не стираючи попереднє. 2>&1 дописує туди ж потік помилок — без цього в лог потрапить тільки успішний вивід, а падіння лишиться невидимим. Саме 2>&1 забувають найчастіше.

перевірити себе
$ mkdir -p ~/logs $ crontab -l 5 * * * * /bin/bash /home/<логін>/www/bin/report.sh >> /home/<логін>/logs/report.log 2>&1 $ tail -n 3 ~/logs/report.log 2026-09-15T19:05:02+03:00 ok: 13 відгуків, середня 4.15 2026-09-15T20:05:01+03:00 ok: 13 відгуків, середня 4.15 2026-09-15T21:05:02+03:00 ok: 14 відгуків, середня 4.07

Рядки логу вище — приклад того, як він виглядає у справного конвеєра; у тебе будуть свої числа й дати.

Питання

У терміналі bash bin/report.sh відпрацьовує бездоганно. У логу cron — bin/report.sh: No such file or directory, хоча файл на місці й ти щойно його бачив. У чому річ?

Крок 9

Симулятор оновлення: одна доба життя звіту

Відгуки приходять коли завгодно, cron спрацьовує за розкладом — між ними завжди є зазор

Відгуки не питають про розклад. Одна людина заповнює форму о 09:52, друга — о 21:09. А cron запускається рівно за таблицею.

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

4 14 звіт відстає на 2 відгуки надійшов відгук запуск cron · 5 * * * * 00:0006:00 12:0018:00 24:00 — справжня кількість - - у report.html
Зелена пунктирна лінія ніколи не переганяє синю і не відстає більше ніж на один інтервал розкладу. Це і є ціна автоматизації: звіт завжди трохи в минулому, і ти сам вирішуєш, наскільки. Дані доби вигадані для симулятора нижче.

Прокрути добу сам

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

Перемикачем «стан конвеєра» можна зламати щось о 10:00. Далі подивись, який рядок зʼявиться в логу і що станеться зі сторінкою.

Симулятор оновлення · 15 вересня
Перевірка «даних замало — не чіпати старий звіт» і зупинка на першій же помилці. Вимкни й подивись, що потрапить на сторінку.
00:0006:0012:0018:0024:00
~/data/responses.csv · приватне 4 рядків даних · останній о —
report.html · що бачить відвідувач 4 відгуків · середня —

Розклад: 5 * * * * — 24 запуски за добу. Учора о 23:05 звіт зібрався успішно: 4 відгуки, середня 4.25.
Питання

На сторінці звіту написано «оновлено о 09:05». Зараз 21:00. Числа виглядають свіжими, рядок у crontab -l на місці, помилок ніхто не бачив. Що насправді відбувається?

Крок 10

Свіжість: чому застарілий звіт гірший за відсутній

Сторінка без дати оновлення виглядає свіжою навіть через три місяці

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

На екрані свіжий звіт і звіт тримісячної давності виглядають однаково. Відрізнити їх можна лише за часом останнього оновлення. Тому застарілий звіт гірший за відсутній: він показує неправильні числа так само впевнено, як правильні.

мітка часу в шаблоні
<p class="upd">Оновлено <time datetime="{{UPDATED_ISO}}">{{UPDATED}}</time>
   · звіт формується автоматично щогодини</p>

Тег <time> тримає дві версії одного моменту. Усередині стоїть та, що для людини: 15.09.2026 21:05. В атрибуті datetime — машинна: 2026-09-15T21:05:02+03:00.

Друга версія потрібна не для краси. За нею автоперевірка на уроці 14 порахує вік звіту. І за нею ж це зробить будь-яка програма, що читатиме твою сторінку.

Вік звітуЯк показуватиЧому саме так
до 2 годинзвичайна мітка «оновлено о 21:05»норма для розкладу «щогодини»
2–24 годинимітка плюс «дані станом на вчора»читач має знати, що бачить не сьогодні
понад добувидима позначка «звіт застарів» на самій сторінціконвеєр зламався, і мовчати про це не можна

Третій рядок робить не скрипт звіту — він же в цей момент і не запускається. Його роблять кілька рядків CSS або JS у самому шаблоні. Вони порівнюють datetime з поточним часом браузера.

Мовою продукту це зветься «деградувати чесно». Зламане має виглядати зламаним, а не свіжим.

Друга мітка — для тебе, не для читача Пиши в лог рядок на кожен запуск, а не тільки на помилку. Це найпростіший спосіб стежити за конвеєром. Подивись на ~/logs/report.log: якщо останній запис учорашній, конвеєр стоїть. Слова «error» там може й не бути взагалі. Мовчазний лог — теж симптом.
Питання

Однокласник каже: «мітка часу тільки псує вигляд сторінки — без неї звіт завжди виглядає актуальним». Формально він має рацію: без дати ніхто не дізнається, що дані застаріли. Що з цією логікою не так?

Крок 11

Коли даних мало: середнє без кількості — це не число

«Середня оцінка 5,0» з двох відгуків працює проти тебе, а не на тебе

Середнє арифметичне має неприємну властивість. Воно виглядає однаково солідно і з двох значень, і з двох тисяч. Формула та сама, знаків після коми стільки ж, шрифт великий.

Наскільки цьому числу можна вірити, показує не воно саме. Показує кількість відповідей, яка стоїть поруч.

Що написано у звітіЯк це читає доросла людина
«Середня оцінка: 5,0»невідомо, скільки людей; ідеальна оцінка без обсягу — ознака або дуже маленької вибірки, або підібраної
«Середня оцінка: 5,0 · 2 відгуки»двоє друзів похвалили; чесно, але це ще не дані
«Середня оцінка: 4,07 · 14 відгуків»у цьому вже видно живий продукт: є і задоволені, і незадоволені
«Середня оцінка: 4,04 · 47 відгуків»число, з яким можна працювати й на яке можна посилатися

Третій рядок переконливіший за перший — і це не парадокс. Ідеальні показники на ранньому етапі не викликають довіри. Будь-хто з досвідом знає: продукт, яким справді користувалися чотирнадцятеро, когось таки дратував. Одиниця в розподілі — не пляма, а доказ, що дані справжні.

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

Кількість завжди поруч із середнім. Не в іншому блоці, не дрібним шрифтом унизу — в одному рядку. Технічно це означає, що {{AVG}} і {{COUNT}} у шаблоні стоять поруч і вимикаються тільки разом.

Поріг. Нижче певної кількості середнє не показується взагалі — замість числа стоїть текст. Для класного проєкту розумний поріг — пʼять відгуків. Менше означає, що відповіли лише ті, кого ти особисто попросив.

Поріг задається однією змінною в скрипті. Запиши його ще й у ~/notes/. Тоді на Demo Day ти відповіси на питання «чому саме пʼять».

поріг у report.sh
MIN=5
if [ "$N" -lt "$MIN" ]; then
  AVG_HTML="відгуків поки $N — середню оцінку покажемо від $MIN"
else
  AVG_HTML="середня оцінка $AVG з 5 · $N відгуків"
fi

Розподіл замість одного числа. Середнє 4,0 може означати «всі поставили 4» і може означати «половина поставила 5, половина — 3». Це два зовсім різні продукти, і розподіл їх розрізняє за секунду.

Але діє те саме обмеження. У класі всі одне одного знають. Розподіл із двох відповідей просто показує на автора двійки. Тому поріг для розподілу ставлять вищий, ніж для середнього.

Чому не варто округляти до цілого 4,07 і 4,5 — різні речі, а після округлення обидві стануть «4». Два знаки після коми в середньому — не показна точність, а спосіб не втратити різницю, яку ти сам будеш аналізувати. Інша річ, що третій знак уже бреше: у 14 відгуках його просто немає звідки взяти.
Питання

У твоєму звіті написано «середня оцінка 5,0» — і на Demo Day це відлякало журі замість того, щоб переконати. Чому найкраще можливе число спрацювало проти продукту?

Крок 12

Звіт як частина продукту, а не як домашка

Публічна статистика підвищує довіру — рівно доти, доки числа мають обсяг і пояснення

Сторінка report.html робить із твоїм лендингом те саме, що відгуки роблять із товаром у магазині. Замість «продукт корисний» на сторінці зʼявляється «стільки-то людей уже спробували, і ось що вийшло».

Різниця в тому, хто це каже. Перше кажеш ти. Друге кажуть дані.

Працює це з трьох причин. Перша — перевірюваність. Число можна відкрити й побачити: воно не в презентації, а на сторінці.

Друга — свіжість. Мітка часу показує, що продукт живий, а не покинутий торік. Третя — повнота. У розподілі видно й погані оцінки. А хто показує погані, той навряд чи прикрасив хороші.

ПоказуватиНе показувати на публічній сторінці
кількість відгуківімена, коментарі, час окремої відповіді
середня оцінка поруч із кількістюсередня без кількості або нижче порога
розподіл 5★…1★ повністю, з одиницями«обрізаний» розподіл без низьких оцінок
топ функцій, яких просятьтоп із двох відповідей — це не топ, а збіг
дата останнього оновленняпоказник, який ти не можеш пояснити вголос
як саме зібрані дані, одним реченнямчисло, що без контексту читається як вирок продукту

Останній рядок — найтонший, тож розберімо його на прикладі. «Активних користувачів цього тижня: 3» і «Виторг за місяць: 0 грн» — правдиві числа. Але на публічній сторінці вони нічого не пояснюють.

Читач бачить показник, за яким ще немає обсягу, і робить висновок не на твою користь. Пояснення дрібним шрифтом цього не рятують.

Місце таких чисел — у пітчі. Там ти скажеш поруч: «третій тиждень після запуску, платний тариф ще не ввімкнено».

І це не те саме, що ховати погане. Одиниця в розподілі оцінок лишається на сторінці. Вона про продукт, і поруч із нею стоять інші тринадцять оцінок. А нульовий виторг на другому тижні — не про продукт, а про те, що зарано.

Що з цього поїде на Demo Day На уроці 13 report.html перестане бути окремою сторінкою. Кілька його чисел підуть просто на лендинг — у блок, який показує, що продуктом уже користуються. Тому шаблон варто одразу верстати на токенах brand.css. Тоді переносити доведеться числа, а не оформлення.
Питання

Ти готуєш report.html до Demo Day. Який із чотирьох блоків краще прибрати з публічної сторінки — і при цьому лишити в пітчі?

Питання

Ти додав у форму нове поле «звідки ви про нас дізналися» і поставив його третьою колонкою. Скрипт звіту не змінював. Наступного ранку звіт показує «0 відгуків», хоча CSV росте на очах. Що сталося?

Крок 13

Практика: збираємо конвеєр

Виконуй по черзі. Галочки зберігаються — сторінку можна закрити.

Опорний скрипт цілком

Це той самий код, що збирає конструктор, зведений в один файл. Заміни логін у другому рядку на свій — і далі правь під свої агрегати.

~/www/bin/report.sh · опорна версія
#!/bin/bash
set -euo pipefail
export LC_ALL=C.UTF-8            # без цього tolower не бачить кирилиці

BASE=/home/<логін>          # ← свій логін
SRC="$BASE/data/responses.csv"
TPL="$BASE/www/bin/report.tpl.html"
OUT="$BASE/www/report.html"
MIN=5

[ -r "$SRC" ] || { echo "$(date -Is) немає доступу до $SRC"; exit 1; }
[ -r "$TPL" ] || { echo "$(date -Is) немає шаблона $TPL";     exit 1; }

# кількість і сума оцінок за один прохід
read -r N SUM <<<"$(awk -F',' 'NR>1 && $3+0>=1 && $3+0<=5 { n++; s+=$3 }
                       END { print n+0, s+0 }' "$SRC")"

if [ "$N" -lt "$MIN" ]; then
  echo "$(date -Is) мало даних: $N < $MIN, звіт не перезаписано"
  exit 0
fi

# українська множина: 1 відгук, 2–4 відгуки, решта — відгуків
case "$N" in
  *1[1-4]) WORD="відгуків" ;;   # 11–14 — виняток
  *1)      WORD="відгук"   ;;
  *[2-4])  WORD="відгуки"  ;;
  *)       WORD="відгуків" ;;
esac

AVG=$(awk -v s="$SUM" -v n="$N" 'BEGIN { printf "%.2f", s/n }')

DIST=$(awk -F',' 'NR>1 { r=$3+0; if (r>=1 && r<=5) c[r]++ }
       END { for (i=5; i>=1; i--) printf "<li>%d★ — %d</li>", i, c[i]+0 }' "$SRC")

TOP=$(awk -F',' 'NR>1 { f=tolower($4); gsub(/^[ \t]+|[ \t]+$/,"",f);
                        if (f!="") c[f]++ }
      END { for (k in c) printf "%d\t%s\n", c[k], k }' "$SRC" \
      | sort -rn -k1,1 | head -3 \
      | awk -F'\t' '{ printf "<li>%s — %d</li>", $2, $1 }')

UPD=$(date '+%d.%m.%Y %H:%M')
ISO=$(date -Is)

# атомарний запис: тимчасовий файл поруч із ціллю, потім mv
TMP=$(mktemp "$(dirname "$OUT")/.report.XXXXXX")
trap 'rm -f "$TMP"' EXIT

sed -e "s|{{COUNT}}|$N|g" \
    -e "s|{{COUNT_WORD}}|$WORD|g" \
    -e "s|{{AVG}}|$AVG|g" \
    -e "s|{{DIST}}|$DIST|g" \
    -e "s|{{TOP}}|$TOP|g" \
    -e "s|{{UPDATED}}|$UPD|g" \
    -e "s|{{UPDATED_ISO}}|$ISO|g" \
    "$TPL" > "$TMP"

chmod 644 "$TMP"
mv "$TMP" "$OUT"
trap - EXIT
echo "$ISO ok: $N відгуків, середня $AVG"

trap 'rm -f "$TMP"' EXIT прибирає тимчасовий файл, якщо скрипт упаде посередині: інакше в ~/www/ накопичаться десятки недописаних .report.*. Після успішного mv пастку знімають — файлу з таким імʼям уже немає.

Що робиться локально, а що по FTP і по SSH Локально: пишеш report.tpl.html і report.sh в редакторі. Зберігаєш у UTF-8 без BOM. Переноси рядків мають бути LF, а не CRLF. Інакше bash відповість bad interpreter: /bin/bash^M. По FTP: обидва файли → ~/upload/~/www/bin/. По SSH: chmod +x, mkdir -p ~/logs, crontab -e, перший запуск руками й читання логу. report.html по FTP не заливається ніколи.
Режим «Зламай конвеєр» Учитель підкине одну з чотирьох поломок. Це може бути відносний шлях у crontab, знятий біт виконання, зіпсований плейсхолдер у шаблоні або відсутній 2>&1. Твоє завдання — не вгадати, а знайти місце. Спершу подивись лог. Потім запусти скрипт руками. Потім перевір crontab -l. Порядок тут важливіший за швидкість.
Крок 14

Автоперевірка check11 7, друга частина

Сервер видалить твій report.html, запустить скрипт сам, порахує числа незалежно від тебе й звірить. Спроби не обмежені.

Команда та сама, що й на уроці 11: перевірка check11 7 одна на два уроки. Там вона дивилася на форму, обробник і приватний файл — це була перша частина. Тут дивиться на звіт report.html і розклад запуску. Список критеріїв нижче інший, спільний в обох тільки no_media.

Демонстраційний режим: результат згенеровано для показу. На сервері ця кнопка запускає check11 7 від імені учня і читає ~/.progress/11-7.json.

Як перевіряється no_pii Сервер бере колонку name і колонку comment з твого ~/data/responses.csv і шукає кожне значення в тексті report.html. Один збіг — і критерій світиться червоним: no_pii блокує оцінку, тож роботу поки не приймуть. Прибери зі звіту чуже імʼя чи коментар і запусти перевірку ще раз — спроби не обмежені. Перевірити себе тією самою логікою можна до здачі, командою з кроку 13. Разом із no_media це два критерії, які блокують оцінку.

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

Крок 15

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

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

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

Що тепер уміє твій продукт

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

  1. Додати у звіт четвертий показник — частку оцінок 4–5 у відсотках ({{SHARE45}}): і в шаблон, і в скрипт. Переконатися, що після наступного спрацювання cron число оновилося саме, а не після твого ручного запуску.
  2. Записати у ~/notes/report-notes.md: свій рядок crontab людською мовою, обраний поріг MIN і одне речення — чому саме такий.
  3. Перевірити no_pii самостійно командою з кроку 13 і вставити її вивід (він має бути порожній) у ті самі нотатки.
Анонс уроку 13 Далі — IT-професії майбутнього і збірка лендинга. Розберемо, які ролі потрібні продукту. Зробимо сторінку team.html. І почнемо збирати index.html з усього, що назбиралося за модуль. Числа із сьогоднішнього звіту стануть на головній окремим блоком.
Джерела

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

Кожне твердження, яке ти можеш перевірити, має посилання. Перевіряти дозволено й корисно.

Усі числа в прикладах цієї сторінки — 14 відгуків, середня 4.07, розподіл, топ функцій, час доби в симуляторі, рядки логу — вигадані автором сторінки для показу і підписані як приклад. На твоєму акаунті будуть свої. Імена в схемах теж вигадані. Повний список джерел із поясненнями, що звідки взято, — у файлі urok-12-джерела.md поруч із цією сторінкою.