У ~/data/leads.jsonl уже лежать справжні заявки
з твоєї форми. Сьогодні вони перетворюються на дашборд — і головна робота
не в тому, щоб порахувати, а в тому, щоб порахувати й нічого не винести
назовні.
report.json;Слова «агрегація», «атомарний запис» та «ідемпотентність» поки нічого не означають. Розберемо їх по черзі, кожне на своєму місці.
З теми 2 у тебе вже є приймач. Він дописує рядок у
~/data/leads.jsonl щоразу, коли хтось заповнює форму.
Файл лежить поза ~/www/. Саме тому за посиланням
його ніхто не відкриє.
Сьогодні ти вперше береш дані з приватної частини й кладеш результат
у публічну. Межу між ними проводиш ти сам. Скрипт
~/bin/report.sh читає файл з іменами й адресами, а віддає
report.json із самими числами.
Кожен зайвий ключ у цьому JSON бачить будь-хто у світі. Тому за кожне поле звіту доводиться відповідати окремо.
| Файл | Права | Що в ньому |
|---|---|---|
~/bin/report.sh | 700 | твій скрипт: читає ~/data/leads.jsonl, пише
report.json. По FTP кладеш у ~/upload/,
далі по SSH переносиш у ~/bin/ і ставиш права |
~/www/dash/index.html | 644 | дашборд: діаграма, числа з report.json,
час останнього оновлення, твій коментар до цифр |
~/www/dash/report.json | 644 | генерує сам скрипт. По FTP не заливаєш ніколи — інакше числа на сторінці розійдуться з даними |
~/data/leads.jsonl | 600 | джерело. Лишається там, де лежало. На сервер сьогодні
не заливається й у ~/www/ не потрапляє |
~/www/dash/, і наступного уроку ти працюватимеш
у ній же. Не шукай lesson-09: тема одна, уроків два.report.json вагою
в кілька сотень байтів. Усе, що важче за текст, учитель дивиться очно.| Етап | Хв | Що робиш |
|---|---|---|
| Межа приватного й публічного | 15 | розбираєш шлях даних і вирішуєш, де саме проходить лінія |
| Читання JSONL у bash | 25 | jq, sort | uniq -c, awk;
звіряєш кожне число руками |
Каркас report.sh | 25 | set -euo pipefail, абсолютні шляхи, блоки скрипта |
| Мінімізація й агрегація | 20 | конструктор звіту: що потрапляє в публічний JSON, а що ні |
| Права на скрипт і дані | 15 | аудит прав: що зламається й що витече при кожній комбінації |
| Атомарний запис та ідемпотентність | 15 | тимчасовий файл і mv; два запуски — один результат |
| Дашборд і автоперевірка | 5 | сторінка читає report.json, чеклист до зеленого |
Дані для звіту приватні. Сам звіт публічний. Між ними — рівно один файл, який ти пишеш власноруч
На уроці 3 ти вже клав leads.jsonl поза ~/www/.
Тоді за все відповідав сервер: чого немає у веб-корені, того для браузера
не існує.
Сьогодні захист інший — твій власний код. Скрипт має право читати приватний файл. Він же має право писати в публічну теку. Що саме перетече через нього, вирішує вже не сервер, а ти.
Уяви класний журнал. Один рядок у ньому — це одна людина: прізвище, оцінки, пропуски. А середній бал класу — уже не про людину. Він про всіх разом, і за ним нікого не впізнати.
З нашим файлом так само. Рядок leads.jsonl стосується
однієї людини: там ім'я, адреса, текст повідомлення. Число
"10A": 22 не стосується нікого окремо.
Перетворення записів на такі спільні числа зветься агрегацією. Саме тут і проходить межа між приватним і публічним.
| Запис | Агрегат | |
|---|---|---|
| Про кого | про конкретну людину | про групу |
| Приклад | {"name":"Оля","email":"olia@…"} |
{"10A": 22} |
| Де живе | ~/data/, права 600 |
~/www/dash/, права 644 |
| Що дає стороннім | адресу для листа, привід зателефонувати | нічого, поки група достатньо велика |
| Чи можна відкотити | — | ні: з числа 22 не дістати 22 імені |
Однокласник зробив дашборд швидше за всіх: скрипта в нього немає взагалі.
Він скопіював leads.jsonl у ~/www/dash/, і
index.html читає файл через fetch та рахує суми
просто в браузері. Сторінка працює, числа правильні. Що з цим не так?
Кожен рядок файлу — окремий валідний JSON. Саме тому звіт можна порахувати командами, а не програмою
Формат JSON Lines зроблений так, щоб його можна було читати
рядок за рядком і дописувати в кінець без переписування файлу. Одна заявка —
один рядок, розділювач — \n. Ніяких ком між об'єктами і жодних
квадратних дужок навколо.
{"ts":"2026-08-30T09:14:07+00:00","name":"Оля","email":"olia@example.com","class":"10A","message":"Хочу в гурток робототехніки"}
{"ts":"2026-08-30T09:16:31+00:00","name":"Тарас","email":"taras@example.com","class":"10B","message":""}
{"ts":"2026-08-30T09:22:04+00:00","name":"Ніна","email":"nina@example.com","class":"10A","message":"Чи потрібен свій ноутбук?"}
wc -l і його пасткаwc -l рахує не рядки, а символи переводу рядка. Різниця
вилазить на останньому записі. Приймач міг упасти на півдорозі й не дописати
\n у кінці.
Тоді запис на диску є, а перевід рядка після нього — ні. Ти побачиш 46 замість 47 і нічого не запідозриш. Команда, яка рахує чесно:
Порожній шаблон '' збігається з будь-яким рядком, а
grep рахує саме рядки, а не переводи. Різниця між двома
командами — це і є твій зіпсований останній запис.
На одному й тому самому файлі wc -l каже 46, а
grep -c '' — 47. Ти відкриваєш файл і рахуєш очима: записів
справді 47, усі цілі. Обидві команди відпрацювали без помилок.
Що не так із файлом?
jqjq — це grep для JSON. Ключ -r віддає
рядок без лапок, щоб його можна було передати далі по конвеєру.
sort | uniq -cuniq склеює тільки сусідні однакові рядки. Без
sort перед ним ти отримаєш той самий клас у кількох групах.
Це та сама команда, якою ти звіряєш звіт руками. У скрипт іде інша — та, що одразу дає готовий JSON:
Ця команда робить чотири кроки поспіль. Ключ -s (від slurp,
«сьорбнути») читає весь файл одразу й складає рядки в один масив. Без нього
jq обробляв би кожен рядок окремо й не зміг би їх порівняти.
Далі group_by розкладає масив на купки за значенням поля.
Усі заявки з 10A — в одну купку, з 10B — в іншу. Порахувати їх поки ніхто
не просив.
Потім map({(.[0].class): length}) проходить по кожній купці
й лишає від неї два значення: назву класу і length — скільки
в ній заявок. Самі заявки на цьому кроці зникають.
Нарешті add зсипає ці дрібні об'єкти в один. Саме він
і поїде у звіт.
class, jq віддасть
null — і в звіті з'явиться цілком серйозна група
"null": 3. Це не помилка jq: він чесно показав те,
що є в даних. Або відсівай такі записи через
select(.class != null), або зводь їх у групу
"не вказано" — але свідомо.awkКоли потрібна сума або середнє, а не кількість, рахує awk.
Він читає потік рядок за рядком, накопичує в змінних і друкує підсумок
у блоці END.
Зверни увагу: жоден символ самого повідомлення сюди не потрапив. Лишилася тільки його довжина. Ось як виглядає агрегація на практиці: текст стає числом якнайраніше, ще всередині конвеєра.
head -3 ~/data/leads.jsonl | jq -r '.class' | sort | uniq -c.
Якщо на трьох рядках результат очевидно правильний, а на 47 — дивний,
проблема в даних, а не в команді.Ти рахуєш унікальних відправників командою
jq -r '.email' ~/data/leads.jsonl | uniq | wc -l і отримуєш 47 —
рівно стільки, скільки всього рядків. Однокласниця з тим самим файлом
отримала 43. Хто помилився і в чому?
set -euo pipefail: три літери проти тихих поломокЗа замовчуванням bash не зупиняється на помилці. Він іде далі й доводить справу до кінця — навіть якщо всі числа вже нулі
Скрипт без цього рядка поводиться так. Команда впала, він написав щось
у stderr і перейшов до наступної. Так само з другою,
третьою і п'ятою.
На виході ти маєш report.json, де все на місці, крім змісту.
Дашборд покаже нулі. А ти шукатимеш помилку в дашборді — і не знайдеш,
бо він ні до чого.
| Прапорець | Що робить | Що ловить у нашому скрипті |
|---|---|---|
-e |
зупиняє скрипт на першій команді, що завершилась не нулем | jq подавився зіпсованим рядком — скрипт зупиняється,
а не пише в звіт половину чисел |
-u |
звертання до невизначеної змінної — помилка, а не порожній рядок | описався в OUT_DIR → OUTDIR: без
-u це порожній рядок, і mv цілиться
в /report.json |
-o pipefail |
конвеєр повертає код найправішої команди, що впала, а не останньої | jq … | sort | uniq -c: jq упав,
uniq відпрацював на порожньому вводі й повернув 0 —
без pipefail помилки просто немає |
-e: grep -c
Команда grep -c '' на порожньому файлі друкує 0
і завершується кодом 1 — «нічого не знайдено». Це не помилка, але
set -e сприймає її саме так і вбиває скрипт. Тому в рядку підрахунку
дописують || true: явно кажемо, що нуль записів — теж результат.Скрипт відпрацював, у консоль нічого не написав, повернув код 0.
У report.json свіжа дата й усі метрики по нулях, хоч у
leads.jsonl 47 рядків. Який рядок ти шукаєш першим?
chmod — до mv, а не після.Цей код можна згенерувати з Antigravity — але вчитель попросить пояснити будь-які три рядки. Тому читай зараз, а не на перевірці.
#!/usr/bin/env bash # ~/bin/report.sh — рахує числа з приватних даних і кладе їх у публічну теку set -euo pipefail # 1. шляхи тільки абсолютні: з cron поточна тека буде іншою SRC="$HOME/data/leads.jsonl" OUT_DIR="$HOME/www/dash" OUT="$OUT_DIR/report.json" # 2. перевірки до першого підрахунку, кожна зі своїм кодом виходу command -v jq >/dev/null || { echo "report.sh: немає jq" >&2; exit 3; } [ -r "$SRC" ] || { echo "report.sh: не читається $SRC" >&2; exit 4; } mkdir -p "$OUT_DIR" # 3. агрегація: у змінних лише числа, жодного імені й жодної адреси total=$(grep -c '' "$SRC" || true) unique_people=$(jq -r '.email' "$SRC" | sort -u | wc -l) with_message=$(jq -s '[.[] | select((.message // "") != "")] | length' "$SRC") avg_msg_len=$(jq -r '.message | length' "$SRC" \ | awk '{s+=$1; n++} END {printf "%.1f", (n ? s/n : 0)}') by_class=$(jq -s 'group_by(.class) | map({(.[0].class // "не вказано"): length}) | add' "$SRC") by_day=$(jq -s 'map(.ts[0:10]) | group_by(.) | map({(.[0]): length}) | add' "$SRC") # 4. збираємо JSON у тимчасовий файл ПОРУЧ із робочим TMP=$(mktemp "$OUT_DIR/.report.XXXXXX") trap 'rm -f "$TMP"' EXIT jq -n \ --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --argjson total "$total" \ --argjson unique_people "$unique_people" \ --argjson with_message "$with_message" \ --argjson avg_msg_len "$avg_msg_len" \ --argjson by_class "$by_class" \ --argjson by_day "$by_day" \ '{generated_at:$generated_at, total:$total, unique_people:$unique_people, with_message:$with_message, avg_msg_len:$avg_msg_len, by_class:$by_class, by_day:$by_day}' > "$TMP" # 5. права ДО підміни, інакше сторінка секунду віддає 403 chmod 644 "$TMP" mv "$TMP" "$OUT" trap - EXIT echo "report.sh: $total записів → $OUT"
jq -n, а не echo з рядком
jq -n будує JSON із параметрів і сам ставить лапки, коми й
екранування. Склеювання рядків руками працює рівно доти, доки в даних
немає лапки, апострофа чи перенесення рядка. Щойно вони трапляться,
report.json стане невалідним, і дашборд покаже помилку розбору.
Ключ --arg передає значення як рядок. А --argjson
передає його як готовий JSON: число або цілий об'єкт.wc -l друкує число з відступом (" 43").
--argjson це ще проковтне: пробіли в JSON дозволені.
А от порівняння рядків уже ні. На сервері (Linux, GNU coreutils) відступу немає.
Якщо пишеш і тестуєш скрипт на своєму Mac, додай | tr -d ' '
у кінець конвеєра.У класі скрипт працює бездоганно: заходиш по SSH, набираєш
./report.sh — звіт оновлюється. Наступного тижня той самий
скрипт ставлять на розклад, і щоночі він падає з
«leads.jsonl: No such file or directory». Файл на місці,
права не мінялися. Що в скрипті написано неправильно?
Правило перевірки одне: чи не соромно вивести цей рядок
report.json на проєктор перед усією школою
Це не фігура мови. report.json лежить у веб-корені з правами
644. Його адресу відкриє з телефона будь-хто, хто її знає.
А знати її неважко: посилання на файл стоїть прямо в коді дашборду. Тож публічний звіт і публічний проєктор — це буквально одне й те саме.
| Що є в даних | У публічний звіт | Чому |
|---|---|---|
name | ні | ідентифікує людину прямо; жодна метрика від імені не залежить |
email | ні | ідентифікує людину й одразу дає канал для спаму |
message | ні | текст, написаний для тебе, а не для всіх; у ньому бувають телефони, адреси, чужі імена |
| кількість записів | так | число про групу, а не про людину |
| кількість унікальних адрес | так | саме кількість: жодної адреси в звіті не з'являється |
| розподіл по класах | так, з порогом | поки в групі достатньо людей — число нікого не називає |
| середня довжина повідомлення | так | з числа 96.4 не відновити жодного слова |
ts з точністю до дня | так | дата — так; точний час до секунди разом із класом уже звужує коло до однієї людини |
report.json,
а не leads.jsonl: у звіті просто немає чого викрадати.Учень додав у report.json ключ
"names": ["Оля","Тарас","Ніна", …] — «щоб на дашборді було
видно, хто вже записався, це ж просто імена, без адрес». Сторінку відкрив
лише вчитель. У чому проблема, якщо адрес там справді немає?
Магічної цифри тут немає. Статистичні служби публікують таблиці про людей щодня. Клітинку, у якій менше 5 або 10 спостережень, вони зазвичай не показують.
Замість неї ставлять зведений рядок або прочерк. Логіка проста: чим менша група, тим легше вгадати, хто саме в ній.
У нашому випадку «додаткові знання» — це вся школа. Якщо в звіті написано
"10C": 1, то будь-хто з 10C, хто чув розмову на перерві, знає
прізвище. Тому в скрипті ставлять поріг:
# групи від 5 осіб показуємо як є, решту зводимо в «інші»
by_class=$(jq -s --argjson min 5 '
group_by(.class)
| (map(select(length >= $min) | {(.[0].class): length}) | add // {}) as $big
| (map(select(length < $min) | length) | add // 0) as $small
| $big + (if $small > 0 then {"інші": $small} else {} end)
' "$SRC")
На нашому наборі {"10A":22,"10B":21,"10C":4} стає
{"10A":22,"10B":21,"інші":4}. Сума не змінилася: 47 лишається 47.
Зник тільки рядок, який називав чотирьох людей з маленького класу.
Оператор // у коді страхує від порожнечі. Якщо великих груп
немає взагалі, add поверне null. Тоді
// {} підставить порожній об'єкт, і скрипт не впаде.
Дашборд показує розподіл по класах із порогом 5. У понеділок там було
{"10A":22,"10B":21,"інші":4}, у вівторок —
{"10A":22,"10B":21,"10C":5}. Хтось, хто дивиться дашборд
щодня, тепер знає щось нове про конкретну людину. Що саме?
Обираєш показники — сторінка пише за тебе bash
і одразу показує, який report.json із цього вийде
і що з нього побачить сторонній
Набір даних тут демонстраційний і зашитий у сторінку: 47 заявок,
43 різні адреси, класи 10A / 10B / 10C. Код, який видає конструктор, —
справжній: його можна вставити у свій report.sh і замінити
назви полів на свої.
report.jsontotal · рахує grep -c ''
unique_people · jq дістає адреси,
sort -u прибирає повтори, у звіт іде тільки кількість
by_class · тут працює поріг малих груп
by_day · дата з ts, обрізана до дня
with_message · саме кількість, не самі повідомлення
avg_msg_len · рахує awk по довжинах
~/www/dash/report.json
Учениця поклала список імен у report.json. Посилання
на цей файл немає ніде: сторінка підтягує його скриптом, а адресу
вона нікому не давала. Через два тижні файл знаходять пошуком
у Google. Як це сталося?
report.sh — це 700У скрипті немає паролів. У ньому є дещо цінніше — карта: де лежать дані, як вони називаються і що в них усередині
Твій сервер не належить тобі одному. Це гуртожиток: у теці
/home/ лежать десятки чужих домашніх тек. Усі ці люди залогінені
на ту саму машину, що й ти.
Права 755 означають одне: кожен із них прочитає твій
файл. Не через інтернет, а просто командою cat, не встаючи
зі свого місця.
| Вісімкове | Літерами | Хто що може |
|---|---|---|
| 700 | rwx------ | власник читає, пише, запускає. Решта — нічого |
| 750 | rwxr-x--- | плюс група: читає й запускає |
| 755 | rwxr-xr-x | плюс усі інші користувачі сервера: читають і запускають |
| 600 | rw------- | власник читає й пише, але не запускає: немає біта x |
| 777 | rwxrwxrwx | будь-хто на сервері може переписати твій скрипт |
~/upload/
FTP-клієнт часто ставить свої права на щойно залитий файл — типово 644 або
навіть 664. Тому файл спершу приземляється в ~/upload/, а вже
по SSH ти переносиш його в ~/bin/ і сам призначаєш права.
Заливати одразу в ~/bin/ і сподіватися на FTP-клієнта —
найчастіша причина, чому критерій script_perms червоний.x
Скрипт без права на виконання все одно можна запустити:
bash ~/bin/report.sh працює. Запускається тут
bash, а сам скрипт для нього — просто текстовий файл на вхід.
А от ./report.sh і cron без біта x
не працюють. Тому 700, а не 600: перша цифра
має бути 7, і саме її перевіряє критерій.~/bin/ не має бути жодного значення з ~/.env:
ні токена вебхука, ні пароля. Якщо скрипту колись знадобиться секрет —
він читає його з ~/.env у момент запуску, а не носить у собі.
Це блокуючий критерій no_secret_in_script, і перевіряється він
буквально пошуком значення токена у файлах ~/bin/.Ти виставив на скрипт права 600 — «щоб уже точно ніхто
не прочитав». bash ~/bin/report.sh відпрацьовує, звіт
свіжий, усе гаразд. Наступного уроку ти ставиш той самий скрипт
у cron, і він мовчить щоночі. Чого не вистачає?
Нижче — шість файлів твого проєкту в тому стані, у якому їх зазвичай лишає FTP-клієнт. Постав кожному права й дивись на наслідок
Помилки тут бувають двох протилежних сортів. Занадто суворі права
ламають те, що мало працювати: веб-сервер не читає звіт, cron
не запускає скрипт.
Занадто вільні віддають чужим те, що мало лишитися твоїм. Тому правильний набір — не «якнайменше», а «рівно стільки, скільки потрібно кожному учаснику».
| Файл | Права | Наслідок |
|---|
report.json віддає не ти, а процес веб-сервера — це
окремий користувач системи, і для нього ти «інші». Тому останній розряд
у публічних файлах — 4: читати можна, писати не можна.
Це, до речі, і відповідь на питання «чому не 600»: 600 означає «навіть
веб-сервер не читає», і сторінка чесно віддає 403.stat на macOS пишеться інакше
На сервері (Linux) — stat -c %a файл. На macOS той самий
результат дає stat -f %A файл. Скопіював команду з уроку
в термінал свого Mac і бачиш «illegal option -- c»? Річ саме в цьому,
а не в правах.Дашборд відкривається, діаграма на місці, а замість чисел — прочерки.
У консолі браузера: GET /dash/report.json 403 (Forbidden).
Скрипт при цьому відпрацював без жодної помилки й щойно оновив файл.
Що сталося і в якому рядку скрипта це виправляти?
mvПоки твій скрипт пише звіт, дашборд може його читати. Це не гіпотеза — це кожен запуск
Уяви, що стираєш дошку й пишеш на ній нове оголошення. Хвилину дошка напівпорожня. Хто зайде саме тоді, прочитає половину фрази й піде.
Команда jq … > report.json робить рівно те саме. Оболонка
спершу обнуляє файл, а потім наповнює його по частинах. Між цими
двома моментами на диску лежить обрізаний JSON.
Хто відкриє дашборд у цю мить, отримає помилку розбору. Виглядає це як випадковий глюк, а насправді трапляється при кожному запуску.
mv — це одна діяТому оголошення пишуть на аркуші за столом, а на дошку вішають уже готовим. Обмін займає секунду, і напівпорожньої дошки ніхто не бачить. Це і зветься атомарним записом: дія або сталася повністю, або не сталася зовсім.
Перейменування файлу в межах однієї файлової системи — це системний
виклик rename(). Ядро гарантує ось що. У будь-яку мить ім'я
report.json веде або на старий файл цілком, або на новий цілком.
Стану «наполовину» не існує в принципі.
/tmp
Гарантія працює тільки в межах однієї файлової системи. Тека
/tmp часто лежить на іншому розділі. Поклади тимчасовий файл
туди — і mv перетвориться на «скопіювати й видалити».
Це знову поступовий запис із тим самим вікном. Тому в скрипті стоїть
mktemp "$OUT_DIR/.report.XXXXXX": тимчасовий файл народжується
в тій самій теці, куди й ляже.trap
Ім'я починається з крапки, щоб недописаний файл не потрапив у списки й
не привертав уваги. А trap 'rm -f "$TMP"' EXIT прибирає його,
якщо скрипт упав на півдорозі. Без цього рядка в теці дашборду
накопичуються шматки звітів — по одному після кожної невдачі.Запусти скрипт двічі, нічого не міняючи в даних. Звіт має вийти той самий, до останнього числа. Якщо так — скрипт ідемпотентний.
Найчастіші причини, чому цього не стається:
>> замість > — числа не замінюються,
а дописуються, і файл швидко перестає бути валідним JSON;report.json і додає до нього —
метрики ростуть при кожному запуску без жодних нових заявок;leads.jsonl «для перевірки» —
джерело даних скрипт звіту не змінює ніколи.Перевірка займає три команди. Поле generated_at міняється
завжди — його з порівняння прибирають:
jq -S сортує ключі, щоб diff не спрацював
на різному порядку полів. Порожній вивід diff і слово
«ідемпотентно» — це і є критерій idempotent.
Дашборд відкривається нормально. Але приблизно раз на двадцять перезавантажень показує «помилка розбору JSON». Натискаєш оновлення ще раз — усе вже гаразд. Однокласник каже: «глюк браузера, не зважай». Що насправді відбувається?
Учень запустив скрипт тричі поспіль, нічого не змінюючи в даних.
total у звіті: 47, потім 94, потім 141.
Що він написав у скрипті?
Уся арифметика вже сталася на сервері. Сторінці лишається прочитати шість чисел і намалювати з них картинку
Це і є розподіл праці, заради якого існує report.sh. Сторінка
не має доступу до leads.jsonl — і не потребує його. Вона тягне
report.json вагою в кілька сотень байтів, тому відкривається
миттєво навіть із телефона на поганому зв'язку.
fetch('report.json', {cache: 'no-store'})
.then(r => r.ok ? r.json() : Promise.reject('HTTP ' + r.status))
.then(d => {
document.getElementById('total').textContent = d.total;
document.getElementById('uniq').textContent = d.unique_people;
// час беремо зі звіту, а не з годинника відвідувача
document.getElementById('upd').textContent =
new Date(d.generated_at).toLocaleString('uk-UA');
drawBars(d.by_class); // твій SVG, як на уроці 6
})
.catch(e => {
document.getElementById('err').textContent = 'звіт недоступний: ' + e;
});
cache: 'no-store' — не дрібниця
Браузер із задоволенням покаже вчорашній report.json із кеша:
ім'я файлу не змінилося, отже «це той самий файл». Ти оновлюєш звіт щогодини,
а відвідувач бачить учорашні числа й свіжу дату поруч. Без цього прапорця
дашборд бреше найнеприємнішим чином — переконливо.generated_at ставить сам скрипт у момент запису, у UTC
(date -u), а сторінка перетворює його на місцевий час
відвідувача. Свій годинник браузер для цього не питає — тільки для
переведення в локальний пояс.report.json; якщо його там немає,
коментар вигаданий.Ти оновив дані, запустив скрипт, відкрив дашборд — числа старі.
Відкриваєш report.json прямою адресою в тій самій вкладці —
числа теж старі. Заходиш по SSH, робиш cat того самого
файлу — числа нові. Файл один. Де загубилися числа?
report.shКроки 1—9 робиш у себе на комп'ютері й у терміналі, кроки 10—15 — на сервері. Галочки зберігаються
report.sh,
index.html і, якщо є, README.md.
А report.json створює сам скрипт. Заливати цей файл руками
не можна ніколи. Рівно з цього починається розбіжність між числами
на сторінці й даними у файлі. Знімки екрана, експорти в PDF і копії
leads.jsonl лишаються на твоєму комп'ютері.Чекер запускає твій скрипт двічі, порівнює звіти, дивиться на права й шукає в публічному JSON те, чого там не має бути. Спроби не обмежені
Демонстраційний режим: результат згенеровано для показу. На сервері
ця кнопка викликає POST /api/check, який запускає
/opt/lessons/09/check.sh від імені учня.
bash -x ~/bin/report.sh і пояснюєш своїми
словами будь-які три рядки з тих, що вивів трасувальник —
включно з тими, які згенерував Antigravity;leads.jsonl;report.json і пояснюєш, чому там немає жодного
поля з leads.jsonl у первісному вигляді;stat -c %a на скрипті, даних і звіті —
і кажеш, чому саме такі цифри;Це те, що бачить учитель, коли виставляє оцінку.
| Складник | Вага | Результат |
|---|
mv у межах однієї файлової системи — одна дія ядра,
і тільки тому дашборд ніколи не бачить недописаного файлу;700 на скрипт ховають не пароль, а карту:
де дані, як називаються поля, коли все це запускається.report.json замість перезапису —
числа ростуть при кожному запуску;data/leads.jsonl) — з
cron скрипт не знайде файл;chmod 644 на тимчасовому файлі — після
mv звіт лишається 600, і дашборд отримує 403;report.json залито по FTP руками — числа на сторінці
живуть окремим життям від даних;~/bin/, права лишилися
від FTP-клієнта — критерій script_perms червоний.Додай до дашборду четверту метрику — таку, якої в каркасі ще немає.
Поясни її в README.md двома абзацами. Перший: що вона показує
і чим корисна саме тобі.
Другий абзац — чому вона нікого не називає. Він обов'язковий. Якщо пояснити не виходить, це сигнал: метрика небезпечна.
Потім перевір поріг. Постав його на 3, на 5 і на 10. Подивись, як щоразу
міняється твій by_class, і запиши в README.md,
яке число ти обрав і чому.
Кожну команду з цієї сторінки можна перевірити в документації, а більшість — просто набрати в терміналі
\n, кодування UTF-8.
jsonlines.orgjq: group_by, --arg і
--argjson, ключ -s, оператор //,
-n, -S — офіційний посібник.
jqlang.github.io/jq/manualset -e, set -u, set -o pipefail —
розділ «The Set Builtin» посібника GNU Bash.
gnu.org/software/bashset -e не завжди робить те, чого від нього чекають —
розбір випадків, зокрема команд, для яких ненульовий код нормальний.
mywiki.wooledge.org/BashFAQ/105grep: 0 — знайдено, 1 — не знайдено,
2 — помилка. Саме тому grep -c на порожньому файлі валить
скрипт із set -e.
gnu.org/software/greprename(2): «If newpath
already exists, it will be atomically replaced». Гарантія діє в межах
однієї файлової системи.
man7.org · rename(2)mktemp — створення тимчасового файлу за шаблоном
з XXXXXX; файл створюється з правами 600.
man7.org · mktemp(1)trap на EXIT — прибирання за собою
незалежно від того, чим скінчився скрипт.
gnu.org/software/bash · Signalschmod,
stat; на macOS stat -f %A замість
stat -c %a.
man7.org · chmod(1),
man7.org · stat(1)k-анонімність — L. Sweeney, «k-Anonymity: A Model
for Protecting Privacy», 2002: запис має бути невідрізненний щонайменше
від k−1 інших.
dataprivacylab.orgfetch і параметр cache — чому
no-store обов'язковий для файлу, який перезаписується
під тим самим іменем.
MDN · Request.cachedate -u +%Y-%m-%dT%H:%M:%SZ і формат ISO 8601 / RFC 3339 —
час у звіті записується в UTC.
datatracker.ietf.org · RFC 3339Повний список із поясненнями, що звідки взято, —
у файлі urok-09-джерела.md поруч із цією сторінкою.