Тема 7, урок 2 з 2. Артефакти: bin/report.tpl.html,
bin/report.sh і згенерований report.html
На минулому уроці форма на твоєму сайті почала писати відповіді в
~/data/responses.csv. Користі з цього файлу поки мало. Щоб
побачити, що люди думають про продукт, треба зайти по SSH і прочитати
файл очима.
Раз на тиждень ти це зробиш. Раз на день — уже ні. А на Demo Day журі спитає: «скільки у вас відгуків і яка середня оцінка». І ти згадуватимеш число з голови.
Сьогодні між приватним CSV і публічною сторінкою стане конвеєр.
Це маленький скрипт, який робить чотири дії підряд. Читає файл із
відповідями. Рахує кілька чисел. Підставляє їх у заготовку сторінки.
І кладе готовий report.html у ~/www/.
А запускати цей скрипт за розкладом буде cron — вбудований
у сервер будильник. Ти ставиш час один раз, далі він працює сам.
З цієї хвилини сторінка звіту оновлюється без тебе.
responses.csv і рахує з нього
кількість відгуків та середню оцінку;| Етап | Хв | Що робимо |
|---|---|---|
| Вхідний зріз | 10 | Тестовий POST по класу — латаємо учорашній обробник |
| Mail merge | 15 | Шаблон із «дірками» — розмічаємо report.tpl.html плейсхолдерами |
report.sh | 30 | Середній рейтинг, розподіл оцінок, ТОП-3 — перевіряємо числа вручну на 3 рядках |
| Тригери й cron | 15 | «За подією» проти «за розкладом» — складаємо свій cron-вираз |
| Практика: розклад | 15 | crontab -e, перенаправлення виводу в лог, перевірка mtime |
| «Зламай конвеєр» | 20 | Читаємо реальні помилки, локалізуємо зламаний вузол, лагодимо |
| Автоперевірка й підсумок | 15 | check11 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.log | cron, при кожному запуску | ~/logs/ | ні, і ніколи не має бути |
Зверни увагу на четвертий рядок: report.html — єдиний файл
проєкту, який ти не пишеш руками. Ти пишеш те, з чого його роблять.
Якщо колись відкриєш його в редакторі й щось поправиш — наступний запуск
cron мовчки затре твою правку. Це не поламка, це те, як живуть згенеровані
файли: правити треба джерело.
report.tpl.html і report.sh ти пишеш
у себе на компʼютері в редакторі. Заливаєш їх по FTP
у ~/www/bin/.
Дві речі FTP-клієнт зробити не вміє. Це право на запуск
(chmod +x) і рядок розкладу в crontab — їх
ставиш по SSH.
А report.html по FTP не заливається взагалі. Перевірка
дивиться, звідки він узявся: якщо його залили, а не згенерував скрипт,
вона попросить переробити.Той самий прийом, що й у злитті листів: бланк із «дірками» плюс таблиця значень
Ідея стара, як друкарський верстат. Є бланк, у якому все постійне вже надруковано, а на місці змінного лишені порожні місця. Є дані — таблиця, де кожен рядок заповнює ці місця по-своєму. Механізм, що складає одне з другим, називають mail merge, злиттям або шаблонізацією — суть однакова.
У нашому випадку бланк — це report.tpl.html. Це справжня
HTML-сторінка: з версткою, підключеним brand.css і заголовками.
Замість чисел у ній стоять помітні маркери на кшталт
{{COUNT}}. Такий маркер зветься плейсхолдер — місце,
куди пізніше стане значення. Самих даних у файлі немає жодних.
Спокуса очевидна: навіщо два файли, якщо можна навчити скрипт друкувати
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 двічі підряд і поїхала верстка. Чому такий підхід
приречений із самого початку?
Конвеєр перетинає цю межу один раз, і що саме через неї пройде — вирішуєш ти
Твій конвеєр складається з чотирьох ланок: приватний CSV → скрипт → шаблон →
публічний report.html. Три перші ланки живуть поза вебкоренем або
містять тільки бланк. Четверта відкрита кожному, хто знає адресу.
Між третьою і четвертою проходить межа публікації.
Рішення, що саме її перетне, ухвалює одна людина. Це ти, і робиш ти це
тоді, коли пишеш скрипт.
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 — буде прозоро, кожен перевірить». Що відповіси?
Число, пораховане з багатьох рядків, не розкладається назад на рядки
Слово «агрегат» звучить бюрократично, а означає просту річ: ти взяв багато окремих записів і замінив їх кількома числами. Кількість, сума, середнє, розподіл, топ — усе це агрегати. Головна їхня властивість — незворотність. З 14 рядків ти зробиш число 4,07 за секунду. З числа 4,07 ти не зробиш назад жодного рядка.
Чесно про межу цього прийому: агрегат ховає людину, поки людей багато. Уяви, що відгуків два. Ти показуєш розподіл «5★ — 1, 2★ — 1» і поруч пишеш «оновлено 14:05». А в класі всі бачили, хто заповнював форму о 14:00.
Така «анонімна статистика» вказує пальцем не гірше за імʼя. Це називають деанонімізацією через малу вибірку.
Рецепт від неї той самий, що й від решти проблем із малими числами, — поріг. Не набралося потрібної кількості відгуків — розподіл не показуємо. Лишається тільки кількість.
У твоєму звіті: 14 відгуків, середня 4,07, розподіл 5★—5, 4★—6, 3★—2, 2★—1, 1★—0. Однокласник каже: «за цими числами я відновлю, хто що поставив». Він має рацію?
Обери, що показує твій звіт, — сторінка збере під це шаблон і скрипт
Далі — робочий інструмент, а не ілюстрація. Познач числа, які хочеш бачити на публічній сторінці. Вкажи, скільки в тебе зараз відгуків. І постав поріг: нижче якої кількості середню оцінку не показувати взагалі.
Внизу зʼявиться готовий report.tpl.html із плейсхолдерами
й report.sh, який їх заповнює. Обидва можна скопіювати
й доробити під свій продукт.
Логін <логін> у шляхах заміни на свій —
інакше скрипт читатиме чужу теку й отримає «Permission denied». Перевір
себе командою whoami.
Ти вмикаєш у конструкторі «динаміку за 7 днів» і «топ-3 функції». Відгуків поки 6. Що з цим не так?
Дві короткі команди читають 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 — це домовленість між обробником форми
і звітом; міняєш одну сторону — міняй і другу.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
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. Перевірити різницю можна прямо зараз:Числа пораховані, шаблон лежить поруч. Лишилося замінити в шаблоні
{{COUNT}} на 14, а {{AVG}} — на середню оцінку.
Руками цього не зробиш: скрипт запускається щогодини, і ніхто не сидітиме
біля нього з мишкою.
Заміну тексту робить команда sed. Вона теж читає файл рядок
за рядком, але не рахує, а міняє одне на інше по дорозі. Сам файл
sed не чіпає: змінений текст він віддає далі, а куди його
покласти — вирішуєш ти.
Запис s|що шукати|на що поставити|g читається просто.
s — це «замінити», від англійського substitute. Риски
відділяють одне від одного. g у кінці означає «в усьому рядку,
а не тільки в першому місці».
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>. Що сталося?
Різниця між > report.html і «тимчасовий файл плюс mv»
Здається, що записати результат — найпростіший крок конвеєра:
sed … > report.html, і все. Проблема в тому, що
> робить дві дії, а не одну. Спершу оболонка відкриває файл
і спорожняє його — файл стає нульового розміру. Лише потім у нього
починає текти вміст. Між цими двома моментами минають мілісекунди, і всі
вони — реальний час, у який файл існує й він порожній.
Девʼятсот девʼяносто девʼять разів із тисячі цього ніхто не побачить. Тисячний раз трапиться тоді, коли сторінку відкриє журі на Demo Day.
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". Рівно
в цю мілісекунду сторінку відкриває людина, якій ти щойно надіслав посилання.
Що вона побачить і як цього уникнути?
cron: розклад, абсолютні шляхи, логПланувальник, який запускає твій скрипт, поки ти на уроці алгебри
У 9 класі ви вже розбирали планувальники докладно. Тому тут буде тільки те, що потрібно для звіту. Плюс одна річ, на якій спотикаються всі.
Розклад описується пʼятьма полями. Читаються вони зліва направо: хвилина, година, день місяця, місяць, день тижня. Зірочка означає «будь-яке значення».
@hourly і
@daily — те саме, що 0 * * * * і
0 0 * * *.0 9 13 * 5 спрацює і
тринадцятого числа, і кожної пʼятниці. Це задокументована поведінка, а не
помилка; на неї натикаються майже всі.Найчастіша скарга уроку звучить так: «у терміналі все працює, а по
розкладу — ні». Причина одна: cron не є твоєю сесією. Коли ти
заходиш по SSH, оболонка читає ~/.bashrc і
~/.profile, підхоплює твій PATH, псевдоніми,
змінні. Cron нічого цього не робить. Він ставить SHELL=/bin/sh.
Бере HOME і LOGNAME із системного файлу
користувачів. І стартує задачу в домашній теці. Твоїх налаштувань
там немає.
| Що ти звик мати | Що бачить cron | Наслідок |
|---|---|---|
| поточна тека — там, де ти був | завжди домашня тека | bin/report.sh шукається як ~/bin/report.sh |
PATH із ~/.bashrc | короткий системний PATH | command not found на власних утилітах |
оболонка bash | /bin/sh | падають [[ ]], масиви, <<< |
| вивід на екран | вивід іде поштою або в нікуди | помилку ніхто не бачить місяцями |
Із цих чотирьох рядків випливає три висновки. Перший:
абсолютні шляхи всюди — і до скрипта в crontab,
і до кожного файлу всередині скрипта. Другий: явно назви програму,
яка виконає скрипт, — /bin/bash у рядку розкладу, а не надія
на #!. Третій: заведи власний лог замість пошти.
5 * * * * /bin/bash /home/<логін>/www/bin/report.sh >> /home/<логін>/logs/report.log 2>&1
>> дописує в кінець файлу, не стираючи попереднє.
2>&1 дописує туди ж потік помилок — без цього в лог
потрапить тільки успішний вивід, а падіння лишиться невидимим. Саме
2>&1 забувають найчастіше.
Рядки логу вище — приклад того, як він виглядає у справного конвеєра; у тебе будуть свої числа й дати.
У терміналі bash bin/report.sh відпрацьовує бездоганно.
У логу cron — bin/report.sh: No such file or directory,
хоча файл на місці й ти щойно його бачив. У чому річ?
Відгуки приходять коли завгодно, cron спрацьовує за розкладом — між ними завжди є зазор
Відгуки не питають про розклад. Одна людина заповнює форму о 09:52, друга — о 21:09. А cron запускається рівно за таблицею.
Тому опубліковане число майже завжди трохи менше за справжнє. Питати треба не «чи є відставання», а «наскільки воно велике й чи це прийнятно».
Тягни повзунок часу. Ліворуч — приватний CSV, який росте від відповідей людей. Праворуч — публічна сторінка, яка змінюється лише в моменти запуску cron. Унизу — лог.
Перемикачем «стан конвеєра» можна зламати щось о 10:00. Далі подивись, який рядок зʼявиться в логу і що станеться зі сторінкою.
5 * * * * — 24 запуски за добу.
Учора о 23:05 звіт зібрався успішно: 4 відгуки, середня 4.25.
На сторінці звіту написано «оновлено о 09:05». Зараз 21:00. Числа
виглядають свіжими, рядок у crontab -l на місці, помилок ніхто
не бачив. Що насправді відбувається?
Сторінка без дати оновлення виглядає свіжою навіть через три місяці
Якщо звіту немає, людина бачить це одразу й іде питати. Якщо звіт є, виглядає акуратно й показує числа — вона їм вірить.
На екрані свіжий звіт і звіт тримісячної давності виглядають однаково. Відрізнити їх можна лише за часом останнього оновлення. Тому застарілий звіт гірший за відсутній: він показує неправильні числа так само впевнено, як правильні.
<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» там може й не бути взагалі.
Мовчазний лог — теж симптом.Однокласник каже: «мітка часу тільки псує вигляд сторінки — без неї звіт завжди виглядає актуальним». Формально він має рацію: без дати ніхто не дізнається, що дані застаріли. Що з цією логікою не так?
«Середня оцінка 5,0» з двох відгуків працює проти тебе, а не на тебе
Середнє арифметичне має неприємну властивість. Воно виглядає однаково солідно і з двох значень, і з двох тисяч. Формула та сама, знаків після коми стільки ж, шрифт великий.
Наскільки цьому числу можна вірити, показує не воно саме. Показує кількість відповідей, яка стоїть поруч.
| Що написано у звіті | Як це читає доросла людина |
|---|---|
| «Середня оцінка: 5,0» | невідомо, скільки людей; ідеальна оцінка без обсягу — ознака або дуже маленької вибірки, або підібраної |
| «Середня оцінка: 5,0 · 2 відгуки» | двоє друзів похвалили; чесно, але це ще не дані |
| «Середня оцінка: 4,07 · 14 відгуків» | у цьому вже видно живий продукт: є і задоволені, і незадоволені |
| «Середня оцінка: 4,04 · 47 відгуків» | число, з яким можна працювати й на яке можна посилатися |
Третій рядок переконливіший за перший — і це не парадокс. Ідеальні показники на ранньому етапі не викликають довіри. Будь-хто з досвідом знає: продукт, яким справді користувалися чотирнадцятеро, когось таки дратував. Одиниця в розподілі — не пляма, а доказ, що дані справжні.
Кількість завжди поруч із середнім. Не в іншому блоці, не дрібним
шрифтом унизу — в одному рядку. Технічно це означає, що
{{AVG}} і {{COUNT}} у шаблоні стоять поруч і
вимикаються тільки разом.
Поріг. Нижче певної кількості середнє не показується взагалі — замість числа стоїть текст. Для класного проєкту розумний поріг — пʼять відгуків. Менше означає, що відповіли лише ті, кого ти особисто попросив.
Поріг задається однією змінною в скрипті. Запиши його ще й у
~/notes/. Тоді на Demo Day ти відповіси на питання
«чому саме пʼять».
MIN=5 if [ "$N" -lt "$MIN" ]; then AVG_HTML="відгуків поки $N — середню оцінку покажемо від $MIN" else AVG_HTML="середня оцінка $AVG з 5 · $N відгуків" fi
Розподіл замість одного числа. Середнє 4,0 може означати «всі поставили 4» і може означати «половина поставила 5, половина — 3». Це два зовсім різні продукти, і розподіл їх розрізняє за секунду.
Але діє те саме обмеження. У класі всі одне одного знають. Розподіл із двох відповідей просто показує на автора двійки. Тому поріг для розподілу ставлять вищий, ніж для середнього.
У твоєму звіті написано «середня оцінка 5,0» — і на Demo Day це відлякало журі замість того, щоб переконати. Чому найкраще можливе число спрацювало проти продукту?
Публічна статистика підвищує довіру — рівно доти, доки числа мають обсяг і пояснення
Сторінка report.html робить із твоїм лендингом те саме,
що відгуки роблять із товаром у магазині. Замість «продукт корисний»
на сторінці зʼявляється «стільки-то людей уже спробували, і ось що вийшло».
Різниця в тому, хто це каже. Перше кажеш ти. Друге кажуть дані.
Працює це з трьох причин. Перша — перевірюваність. Число можна відкрити й побачити: воно не в презентації, а на сторінці.
Друга — свіжість. Мітка часу показує, що продукт живий, а не покинутий торік. Третя — повнота. У розподілі видно й погані оцінки. А хто показує погані, той навряд чи прикрасив хороші.
| Показувати | Не показувати на публічній сторінці |
|---|---|
| кількість відгуків | імена, коментарі, час окремої відповіді |
| середня оцінка поруч із кількістю | середня без кількості або нижче порога |
| розподіл 5★…1★ повністю, з одиницями | «обрізаний» розподіл без низьких оцінок |
| топ функцій, яких просять | топ із двох відповідей — це не топ, а збіг |
| дата останнього оновлення | показник, який ти не можеш пояснити вголос |
| як саме зібрані дані, одним реченням | число, що без контексту читається як вирок продукту |
Останній рядок — найтонший, тож розберімо його на прикладі. «Активних користувачів цього тижня: 3» і «Виторг за місяць: 0 грн» — правдиві числа. Але на публічній сторінці вони нічого не пояснюють.
Читач бачить показник, за яким ще немає обсягу, і робить висновок не на твою користь. Пояснення дрібним шрифтом цього не рятують.
Місце таких чисел — у пітчі. Там ти скажеш поруч: «третій тиждень після запуску, платний тариф ще не ввімкнено».
І це не те саме, що ховати погане. Одиниця в розподілі оцінок лишається на сторінці. Вона про продукт, і поруч із нею стоять інші тринадцять оцінок. А нульовий виторг на другому тижні — не про продукт, а про те, що зарано.
report.html перестане бути окремою сторінкою.
Кілька його чисел підуть просто на лендинг — у блок, який показує,
що продуктом уже користуються.
Тому шаблон варто одразу верстати на токенах brand.css.
Тоді переносити доведеться числа, а не оформлення.Ти готуєш report.html до Demo Day. Який із чотирьох блоків
краще прибрати з публічної сторінки — і при цьому лишити в пітчі?
Ти додав у форму нове поле «звідки ви про нас дізналися» і поставив його третьою колонкою. Скрипт звіту не змінював. Наступного ранку звіт показує «0 відгуків», хоча CSV росте на очах. Що сталося?
Виконуй по черзі. Галочки зберігаються — сторінку можна закрити.
Це той самий код, що збирає конструктор, зведений в один файл. Заміни логін у другому рядку на свій — і далі правь під свої агрегати.
#!/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
пастку знімають — файлу з таким імʼям уже немає.
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.
Порядок тут важливіший за швидкість.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 це два критерії, які блокують
оцінку.crontab -l — з абсолютними шляхами і
2>&1 у кінці рядка;~/logs/report.log із реальними запусками за
останні години, а не одним рядком від ручного запуску;report.tpl.html: у ньому плейсхолдери, а не
числа — тобто скрипт не зʼїв власний шаблон;Це те, що бачить учитель, коли виставляє оцінку.
| Складник | Вага | Результат |
|---|
{{SHARE45}}): і в шаблон, і в скрипт. Переконатися, що
після наступного спрацювання cron число оновилося саме, а не після
твого ручного запуску.~/notes/report-notes.md: свій рядок
crontab людською мовою, обраний поріг MIN і
одне речення — чому саме такий.no_pii самостійно командою з кроку 13 і
вставити її вивід (він має бути порожній) у ті самі нотатки.team.html. І почнемо
збирати index.html з усього, що назбиралося за модуль.
Числа із сьогоднішнього звіту стануть на головній окремим блоком.Кожне твердження, яке ти можеш перевірити, має посилання. Перевіряти дозволено й корисно.
@hourly і
@daily, правило «або» для двох полів дня — довідка
crontab(5).
man7.org · crontab(5)SHELL=/bin/sh, а HOME і
LOGNAME бере з системного файлу користувачів; вивід задачі
йде поштою власнику crontab — той самий crontab(5).
Твої ~/.bashrc і ~/.profile у переліку відсутні —
саме тому їхніх налаштувань у задачі немає.
manpages.debian.org · crontab(5)newpath уже існує, він буде
атомарно замінений, і немає моменту, у який інший процес побачив би його
відсутнім»; rename() не працює між різними файловими
системами (EXDEV) —
man7.org · rename(2)mktemp створює тимчасовий файл безпечно; шаблон має
містити щонайменше три X в останньому компоненті імені —
man7.org · mktemp(1)sed працює порядково; роздільник у команді s
можна замінити будь-яким символом; & у правій частині
означає весь знайдений збіг — довідник GNU sed.
gnu.org · sed · команда sawk: NR, -F, асоціативні
масиви, tolower, printf — POSIX-специфікація
мови.
pubs.opengroup.org · awktolower працює за правилами поточної локалі — у локалі
C кирилиця не змінюється — довідник GNU Awk, розділ про
рядкові функції та локалі. Перевірено командою
LC_ALL=C awk 'BEGIN{print tolower("Експорт")}' на GNU Awk 5.4.0:
у локалі C виводиться «Експорт», у C.UTF-8 —
«експорт».
gnu.org · gawk · String Functions,
gnu.org · gawk · Localesset -e, set -u, pipefail і
trap — довідник GNU Bash.
gnu.org · bash · The Set Builtin> спорожняє файл при відкритті,
>> дописує, 2>&1 зливає потік
помилок у той самий файл —
gnu.org · bash · Redirections<time> і машинний формат атрибута
datetime — специфікація HTML.
html.spec.whatwg.org · the time element2026-09-15T21:05:02+03:00 —
RFC 3339, профіль ISO 8601 для інтернету.
datatracker.ietf.org · RFC 3339644 для файлу, який має віддавати вебсервер,
і що робить chmod — довідник GNU coreutils.
gnu.org · File permissionsУсі числа в прикладах цієї сторінки — 14 відгуків, середня
4.07, розподіл, топ функцій, час доби в симуляторі, рядки логу — вигадані
автором сторінки для показу і підписані як приклад. На твоєму акаунті
будуть свої. Імена в схемах теж вигадані. Повний список джерел із
поясненнями, що звідки взято, — у файлі
urok-12-джерела.md поруч із цією сторінкою.