Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
- ActiveRecord
- AmazonRedshift
- API
- AppleScript
- AWS
- AWSLambda
- CGO
- Chezmoi
- CI
- Clojure
- Cloudflare
- CloudflarePages
- CssParser
- CтохастичнийТаймтрекер
- DNS
- Docker
- Dotfiles
- Fly.io
- GCP
- Git
- GitHub
- GitHubActions
- Go
- Golang
- GTD
- HomeAssistant
- Hugo
- I18next
- JavaScript
- JSON
- Kafka
- MacMiniВДорозі
- MacOS
- Markdown
- Mastodon
- Obsidian
- ObsidianCanvas
- OmniWOPE
- OpenSearch
- Oura
- Ping
- Plausible
- PostgreSQL
- ReactNative
- Redis
- RSS
- Ruby
- RubyOnRails
- Sintra
- SMTP
- SQL
- SQLite
- Svelte
- Swift
- SwiftData
- SwiftUI
- Telegram
- Terraform
- TLS
- TypeScript
- Vercel
- VPN
- WeightPlot
- WordPress
- XCode
- Адвент2024
- Бази Даних
- БДС
- Безпека
- Блокнот
- Вебтехнології
- ВолюІнформації
- Гаджети
- ДизайнМовПрограмування
- Ігри
- Інструменти
- ІнтеграційніТести
- Кава
- КеруваннняЗадачами
- Кодогенерація
- Криптографія
- Локалізація
- Маркетинг
- МетаПост
- МоїПроєкти
- Навігація
- Оптимізація
- ОсновиІнтернетБезпеки
- Помічник ШІ
- ПомічникШІ
- ПостПроПохід
- Програмування
- Продуктивність
- Проєкти
- Проза
- РДУГ
- Рівночасність
- РобочийКомп
- Розробка
- РозумнийБудинок
- СинПопросивПриготувати
- Сон
- СтохастичнийТаймтрекер
- ХмарніТехнології
24.11.2025
Деякі обовʼязки лід-інженера
-
Давайте з очевидного: писати код. В ідеалі, це якийсь особливо складний код, який більше ніхто не напише. Але може всякий бути. Лід-інженер залишається інженером.
-
Проєктувати архітектуру для інших членів команди, що на практиці значить: взяти складну задачу та придумати, як її розвʼязувати, через простіші елементи.
-
Розподіляти задачі між членами команди, або принаймні давати поради щодо того, кому варто взятися за задачу. Для чого потрібно також розуміти навички та знання членів команди.
-
Бути представником команди в ширших контекстах, наприклад, якщо технічне розуміння потрібне для планування продукту.
-
Забезпечувати комфортну роботу команди — піклуватися про інструменти, CI, зручність розгортування, закриття технічного боргу.
-
Досліджувати перспективні технології, які можна впровадити в команді. Це стосується і майбутніх рішень, і вище згадані інструменти розробників, і можливості покращити код чи інфраструктуру.
-
Бути джерелом знань про архітектуру продукту, давати поради, допомагати з технічними рішеннями.
-
Підтримка документації для розробників, щоб не залишатися єдиним джерелом знань.
-
Переглядати код інших членів команди насамперед на відповідність архітектурі та баченню.
-
Мати та розвивати те саме бачення коду, яке допомагає приймати правильні рішення та забезпечити продукту цілісність.
Що забув?
21.11.2025
Hario Largo - найкращий заварник для чаю
Хтось любить каву, хтось любить чай, дехто не пʼє ані того, ані того, втім є одна правда: способів заварити каву значно більше, і пристроїв більше, і технік, і місць, де люди піклуються про якість кави. Чаю в цьому сенсі поки щастить менше.
(Взагалі, може здаватися, що це завжди так було, але ні — вся сучасна культура обміркованого споживання кави існує трохи понад 20 років. Але зараз не про те.)
Я люблю і каву, і чай. Тільки чай завжди був прозаїчніше: ну хіба що температуру води я обирав належну, а далі… ніяк не міг знайти правильний пристрій. Якщо чай кладеш у ситечко — йому немає місця розкритися. Якщо в чайник — то складно відфільтрувати чаїнки. А ще — як витримати час заварювання, коли я не збираюся пити весь чайник одномоментно?
Отже, щоб не відтягувати далі: я знайшов заварник Hario Largo. Ідея проста: це колба, в якій вільно плавають чаїнки, а коли сплив час заварювання — чай зливається через клапан (та велике сито) в підготований глечик, чайник чи чашку.
З таким заварником тривіально витримати всі вимоги заварювання. Хочеш промити чай — та без проблем. Хочеш взагалі проливний, як пуер чи улун — це теж можна. Оці чайні бутони, які красиво розкриваються в чайнику — будуть всі на виду. А можна поставити настоюватись травʼяний чай на пʼятнадцять хвилин, а потім швиденько злити. Можна заварити одну чашку, а можна на цілий термос.
Мені також дуже важливо, що тут немає пластику — сама колба скляна, фільтр сталевий, основа гумова. Все розбирається та миється. Підставку, яка продається разом, я використовую і для пуровера.
Та от повірте, між просто чаєм та таким, що правильно заварений — океан різниці.
20.11.2025
Вайб-кодінг — для всього неважливого
Потрапив на очі гостренький заголовок: Лінус Торвальдс вважає що вайб-кодінг це добре, поки не використовується для чогось важливого. Звучить як відмова від всієї ідеї — окрім як в контексті навчання.
Проте заголовок відірваний від контексту (можеш послухати саме інтервʼю, чом би й ні.) Але на мою думку, в цьому реченні є інший зміст — навмисний чи ні, я не знаю. Ми всі пишемо нескінченні гори неважливого коду.
Як тільки це збагнеш, програмування з допомогою LLM розкриється новими кольорами. Ти починаєш дивитися на майбутній код критично: чи це такий код, про який я хочу думати? То буду думати. А якщо код нудний та тривіальний — то нехай ШІ його згенерує.
І такого коду дуже багато. Взагалі виявляється, що важливі, архітектурні рішення робляться на вищому рівні, або принаймні можна винести свої міркування на цей вищий рівень, а потім доручати ШІ заповнити пробіли.
А до того є одноразові скрипти та команди. Або задачі, які можна розвʼязати одноразовим скриптом, але оскільки його надто довго писати, ти цього не робив. Та навіть досліджувати проєкти значно легше, коли кожну складнющу команду git тощо може згенерувати ШІ.
Програмісти взагалі занадто привʼязують власну цінність до коду, що повертає нас до того, що варто памʼятати, що ти інженер.
19.11.2025
Елементи безпеки в дизайні мови Swift
Не знаю достатньо про Rust, щоб прокоментувати насущне фіаско з unwrap(). Скажу тільки, що, як на мене, небезпечно називати метод, який може панікнути, такою невинною назвою.
А розповім краще про дизайн Swift - мови, яка дуже схожа на Rust, насправді. (Ви взагалі знаєте, що на Swift можна писати й серверні застосунки? Ось, наприклад, Vapor, а до нього є ORM, Postgres та все як ми звикли.)
Отже. У Swift навколо значення типу Optional. Просто повсюди! Якщо порівняти із, наприклад, Go, то в Go теж навколо “опціональні” значення, тільки перевіряти ми їх будемо неявно (як-от, якщо функція foo() (*bar, error) повернула помилку, то в *bar порожньо, а якщо ні, то щось буде, та на nil можна окремо не перевіряти.) Проґавили — отримали паніку. Ну так Go не називає себе безпечною мовою, там естетика професійного інструменту, до якого ти мусиш вивчити техніку безпеки.
У Swift, якщо лінь танцювати весь танець розгортування Optional, можемо виконати примусове розгортання: bar!. Воно викличе помилку, якщо значення немає. Але дуже важливо, що така форма візуально підкреслює наш намір. А намір тут — не “довірся мені, я знаю що роблю”, а “я відкидаю безпеку! Стережися!” І це дуже важливий елемент дизайну безпечної мови.
Так само у Swift кожна функція, де може бути викликана помилка, повинна бути підписана словом throws, а її виклик - try. Слово try дозволено тільки всередині блоку do ... catch, або всередині функції, яка сама буде throws. Або — сміливий варіант - try! - каже, що нам байдуже на те, що помилка стане критичною.
Не існує абсолютно безпечних мов. Та те, як мова поводиться у небезпечних ситуаціях, і визначає, наскільки ми їй можемо довіряти.
18.11.2025
Ідіосинкразії SwiftUI
Я дуже люблю React за його передбачуваність. Якщо створити компонент-функцію, вся вона буде виконана. Хуки теж мають передбачувані моменти виклику. React не ідеальний, але ось ця прозорість точно його сильна сторона.
У SwiftUI прозорості немає. Життєвий цикл компоненти заплутаний. Хочу я робити запит, який приймає в себе аргумент компоненти, умовно Action.where { $0.contextID == contextID }. Цілком нормальна справа. Написати це прямо в анотації @FetchAll, як каже документація — не вийде. Бо анотації не мають доступу до аргументів — хоч цей запит наче буде виконаний вже в конкретному екземплярі.
Тоді є документація про динамічні запити… це трохи не те, але працювати буде. Воно використовує метод .task. І це ще треба знати — я сьогодні дізнався — що в нього є аргумент id, який насправді працює так само як масив залежностей у React.useEffect. Тобто буде ще й оновлювати запит, якщо цей id зміниться.
Але. Якщо запит розташований в .task, він буде виконаний із затримкою та вам не уникнути початкового вигляду із порожнім результатом. Не ідеально.
Тоді залишається інший спосіб, якого в документації немає — це зробити власний ініціалізатор (конструктор) - він й приймає всі аргументи компоненти — та там завантажувати всі правильні запити. Оце і є вихід.
Тільки й це не все розвʼязує, бо ініціалізатор не має доступу до обʼєктів @Environment (це як контексти в React.) Так що якщо ти раптом береш значення звідти — доведеться робити це в батьківській компоненті та передавати аргументами туди, де збираєшся робити запити.
Складно уявити, що тут теж є концепція, яка все це робить логічним, а не просто збігом обставин реалізації.
Також додам, що XCode це єдине, що дійсно змушує мене замислитись про перехід з MacBook Air до чогось… важчого. А мені не хочеться.
17.11.2025
З JSON до SQLiteData.swift
Вже понад рік я користуюся власним застосунком для GTD. Це, безумовно, успіх — в мене таке може вперше, щоб система так довго не розвалювалася. Але є проблема: він зберігає дані в єдиному файлі JSON.
Мало того, що такого рішення недостатньо, щоб його поширювати, так і мені самому в JSON почало бути тісно. Кожна зміна, навіть така мінімальна як редагування тексту задачі, призводила до збереження всього стану, а потім — як я розумію — широкого перемальовування застосунку. Гальмувати стало ну просто неприйнятно довго.
Отже, на вихідних взявся за переписування всієї моделі даних на повноцінну базу даних. Минулого року я залучив GRDB, а цього разу хотілося випробувати SQLiteData. Ця бібліотека, власне, є надбудовою для GRDB, а відрізняє її можливість синхронізувати базу за CloudKit - тобто забриніла можливість зробити й версію для iOS, що нарешті зробило б цей проєкт продуктом.
Розробити схему даних та перенести туди все з JSON дуже просто. А от де починаються труднощі, так це з переходом з прямого читання даних на запити до бази — заради якого я все це й починав. Бо весь смак бібліотеки SQLiteData в тому, що результати запитів автоматично оновлюються.
Головних перепон тут було дві. Перша — вони використовують багато макросів (для побудови SQL з Swift.) Через них компілятор часто не міг дати нормальну помилку та доводилося її шукати. Друга — з мало відомою бібліотекою болюче не вистачає документації та взагалі пояснень. Та ШІ в таких умовах теж мало допоможе!
Тому доводилося читати код самої бібліотеки та шукати там відповіді навіть на прості нібито питання. Як-от, запит не дозволяє написати $0.completedAt < currentDate, якщо стовпчик completedAt може бути нульовим. (Цей вираз, звісно, не виконується буквально, а перекладається в SQL тими самими макросами.) Та я поки знайшов лише такий дикий синтаксис, щоб це обійти: $0.completedAt.map { $0 < currentDate } ?? false.
Так чи інакше, застосунок принаймні скомпілювався та майже повністю працює.
14.11.2025
Трошки з адміністрування OpenSearch
…Передісторія: так склалося що в одного з індексів OpenSearch цілих 100 шардів на приблизно 5 Гб даних. Стандартні рекомендації — це шарди розміром у 10-50 Гб, тобто десь у 1000 разів більші. Ну буває.
Десь місяців із 9 тому я писав, що це ні на що не впливає, але виявляється, все залежить від навантаження. Отже, стала задача звести кількість шардів до розумної… ну, наприклад, 2.
Робити це “наживу” OpenSearch не вміє. Тобто нам доведеться замінити індекс. (До речі, саме тому в OpenSearch також є псевдоніми індексів (alias) - щоб легко підміняти індекси або цілі набори.)
Є операція reindex. Вона бере кожний документ та копіює його в новий індекс. Триває все це кілька годин. Я вже писав, що все в OpenSearch, що працює на рівні документів — дуже повільне. Під час цього, якщо старий індекс відкритий до запису, накопичуються ще зміни, які наздоганяти мені не захотілося.
Тому є інша операція: shrink. Оце вже краще: вона на рівні структур бази обʼєднує шарди. Працює замість годин — лічені хвилини. Але ж не все так просто.
Перепоною стало те, що для виконання shrink всі шарди індексу повинні бути на одному вузлі. Для того є інструкція прямо в документації вище. Але на практиці в нас OpenSearch починав цей переніс та не закінчував. Виявилося, що є обмеження на кількість шардів на вузол.
Причому (!) в AWS OpenSearch обмеження це — як ми визначили — залежить не від розміру вузла. Та точно не від налаштувань — такі речі AWS не дає змінити, щоб ми не “наробили дурниць”. Дякуємо!
Ні — насправді AWS OpenSearch прагне зберегти баланс в кластері, а саме рівний розподіл шардів між вузлами. Ну й добре… створили ще один, порожній, індекс на 100 шардів. Після чого перенесення справжніх шардів на один вузол завершилося успіхом. Ну а далі й shrink відбувся без подальших сюрпризів. Роботу зроблено.
Це я ще не торкнувся того, наскільки безпорадним є UI OpenSearch Dashboards, в якому одне світле місце - Dev Console, тобто можливість надсилати до кластера команди HTTP API.
13.11.2025
Sintra тепер англійською
Крайніми вихідними тихенько випустили англійський переклад нашого застосунку для ведення бюджету Sintra.
Треба сказати, що почали Сінтру ми, як два наївних дніпровських хлопця, російською. Потім у 2022 терміново переклали українською. Це, я вам скажу, титанічний обсяг роботи, особливо для двох людей посеред війни та проміж всіх інших обовʼязків. Власне, тому робота над проєктом була фактично призупинена. А тепер зʼявилося місце її продовжити.
Може здатися, що додати третю мову там, де є дві — легко. Так, це дійсно незрівнянно простіше, ніж додавати другу мову. Але окрім перекладу, довелося також виконати локалізацію. Наприклад: впровадити підтримку тижня, що починається з неділі. Чи форматування сум, де знак валюти йде спочатку: $100.
Величезною підпорою був ШІ, він і зробив чернетку перекладу, і допоміг Саші з локалізацією. Я практично тільки переглядав зміни… та займався окремо застосунком на React Native. Бо в застосунків для iOS своя система локалізації. А ще є App Store Connect, а там кільканадцять сторінок, де теж можуть ховатися не перекладені рядки. От виявив, що ми забули про переклад посилання на політику приватності — і таке є!
До того ж в App Store є така річ, як “мова за замовчуванням”. Нею застосунок світиться в різних місцях — я вам не поясню, за якими правилами. Так от, щоб змінити цю мову, треба було спочатку застосунок опублікувати з англійською мовою, потім в App Store Connect змінити налаштування — яке, несподівано, увімкнулося негайно, без додаткового випуску. Тільки довелося чекати, поки всілякі кеші прочистяться.
(Нещодавно почув ідею, що краще, якщо всі налаштування до застосунку зберігаються в файлах репозиторію, а не в якихось там панелях. Даний випадок це чудово ілюструє.)
12.11.2025
Рухоме середнє
Є в мене утиліта split_tests, вона вміє розбивати пакет тестів на частини рівної тривалості. Для того тривалість кожного файлу передається в широко відомому форматі JUnit XML.
Та все б гарно, але тести не завжди мають ідеально рівну тривалість. (Особливо інтеграційні.) Та оскільки ми спираємося на результати останнього запуску, де-факто розбіжність між частинами складає до 20%. Знаєте, як дратує покладатися на емпіричні виміри, та все одно отримувати таку розбіжність?
Рішення, яке мені напрошується, це обчислювати усереднені тривалості. Та для того зовсім не обовʼязково зберігати статистику кожного запуску за все життя. Нам знадобиться формула рухомого середнього:
T нова = 0.9 * T стара + 0.1 * T поточна
Рухоме середнє крок за кроком наближає нас до справжньої тривалості. Причому ще й з такою зручною особливістю, що свіжі значення мають більшу значущість, ніж старі.
Отже, тепер у split_tests є новий режим:
split_tests -junit-update=old/*.xml -junit-new=new/*.xml -junit-out=updated.xml
Достатньо цю команду додати в кінець збірки, перед збереженням даних, та тривалість стане стабільніше! Наскільки? Це мені ще доведеться визначити.
11.11.2025
Сила LLM - в поєднанні понять
…Гадаю ще рік чи два тому світ пройшов через те, що велика мовна машина здатна генерувати коломийки з конституції США та поєднувати інші так само не повʼязані речі. Але, то гарно не тільки для забави. Я раз за разом знаходжу користь у поєднанні не повʼязаних понять в роботі та побуті.
Наприклад. Сьогодні оновив Miniflux на fly.io та несподівано він почав викидати помилку store: unable to get entries: pq: time zone "Europe/Kyiv" not recognized. Як завжди в таких ситуаціях, немає в мене часу ретельно перевіряти, в чому тут справа. Починаю шукати в Perplexity.
Перший крок лише підтверджує мою інтуїцію: помилка в тому, що PostgreSQL чомусь застряг зі старою назвою поясу Europe/Kiev. Тут же ж через Perplexity перевіряю: так, моя версія PG дійсно має застарілу базу поясів.
А далі починається поєднання понять. Питаю: як заапгрейдити PG на fly.io? Виявляється, що тільки через створення нового серверу з ручним перенесенням даних — на що в мене точно не вистачить терпіння. Ну, значить, доведеться поки повернути стару назву.
Але як то зробити, якщо застосунок просто не працює — ніяк? Значить, залазимо через базу. А як то зробити на fly.io? Виявилось, є проста команда: fly postgres connect. Складаємо запит до бази та команду, туди-сюди, пояс замінено, застосунок працює. Кризу якщо не пройдено, то принаймні відкладено. (Можливо, перейду на повністю локальне рішення для RSS.)
Все це можна було знайти вручну (або взагалі — знати.) Тільки зайняло б це цілий день, та довелося б обшукати декілька сторінок чи навіть баз документації. А тут ШІ знаходить відповідь за секунди, а проблема розвʼязується за кілька хвилин. Та це потужно.
Ось весь діалог, якщо кому цікаво.

