Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni
- AWS
- CGO
- Chezmoi
- CI
- Cloudflare
- CтохастичнийТаймтрекер
- Docker
- Dotfiles
- Fly.io
- GitHub
- Go
- GTD
- Hugo
- MacOS
- Obsidian
- Plausible
- Redis
- RSS
- RubyOnRails
- SMTP
- Svelte
- SwiftData
- SwiftUI
- TLS
- Vercel
- VPN
- WeightPlot
- WordPress
- КеруваннняЗадачами
- МетаПост
- ОсновиІнтернетБезпеки
- Проєкти
- Рівночасність
- РобочийКомп
- Розробка
30.11.2024
Сховище даних для таймтрекера
Я зараз готуюся до серйозної роботи над застосунком для таймтрекінгу, та напевно почати доведеться з перероблення моделі та сховища даних. Бо SwiftData/CoreData мене все ж бісить. Вона, мабуть, гарна для простого випадку, коли все що потрібно — це показувати екран списку та екран запису. Тоді SwiftData інтегрується безпосередньо у SwiftUI. Але як тільки зʼявляється складніша логіка — як-от агрегація, розрізи, особливо із застосуванням великого обсягу даних — то вона починає тріщати. Тому дивлюся на альтернативи.
З іншого боку в мене є досвід підходу модель в JSON, тобто коли весь стан застосунку завантажується та зберігається однією великою структурою даних. Це зручно, проте також обмежує дані за розміром, бо я подивився та дані про теги за останній рік вже сягнули декількох мегабайтів, та зберігати такий JSON на кожну дію не хочеться.
…Логічною серединою мені бачиться SQLite; зокрема, всіляку статистику можна обчислювати за допомогою SQL, а в памʼять завантажувати тільки те, що буде видно. У Swift є декілька бібліотек для SQLite, я поки схиляюся до Lighter, бо вона надає типізовані записи, але в іншому не перетягує абстракціями.
До речі, в базі CoreData взагалі немає агрегацій. Бо це обʼєктна база; хочеш агрегацій — будь ласка, ходи по графу асоціацій. Наприклад, така тривіальна в SQL задача, як “покажи найбільші 10 тегів за кількістю використань”, тут вимагає зберігання кількості в атрибут-кеш, інакше буде повільно та витратно. І це ще проста задача; а як щодо “покажи карту використань тегу за годинами та днем тижня”? В такому важкому на статистику продукті з SQL піде веселіше.
29.11.2024
Сталкер 2
Що в Сталкері чудово: мальовничі та рідні пейзажі та атмосфера. Відкритий світ, наповнений цікавими місцями. Можна просто вештатись територією, досліджувати памʼятки та час від часу натрапляти на квест, печеру з артефактом, сховок з унікальною зброєю. Але головною нагородою є сам процес та задоволення від відкриття. Сталкер — гра саме про це.
Базова ігрова петля тут відрізняється від традиційної “взяти квест — піти на локацію — виконати — повернутися, отримати винагороду, продати знахідки”, бо ігровий світ великий, а швидкого пересування немає. Більше виходить мандрувати по локаціях та час від часу повертатись на базу.
До того ж в Сталкері майже немає луту на продаж. Та й купувати майже нічого не має сенсу — ти знаходиш більше аптечок, їжі та набоїв ніж можна витратити. Гроші витрачаються на покращення зброї, які не є вирішальними, а також на ремонт, замість якого можна просто знайти щось новіше та крутіше. Чим більше карти дослідиш, тим краще знайдеш зброю та спорядження.
Побічні квести дійсно цікаві (мені запамʼятався один про “гуру”, через поради якого новачки влазять в халепи.) Також сподобались сховки-головоломки: шлях до них змушує поміркувати та приємно дивує.
А головний квест нудний, та не більше ніж просуває нас з локації на локацію. Персонажі та фракції теж клішеві та нецікаві. Старі фракції з першого Сталкера не пояснюються, що принаймні в одному випадку суттєво впливає на розуміння сюжету. Діалогової системи практично немає.
Бої “не чудові, але й не жахливі” (с). З людьми пістолет з глушником поки OP. Як і те, що противники здатні поцілити тебе через найменшу щілину в стіні, аби був line of sight. Але є ще мутанти/монстри… В них (поки?) надвисокий ресурс життя. до того ж більшість з них вступає в ближній бій, а рухливість тут…. не така, як в Doom Eternal. Крутитися та шукати під собою мутанта — нічого веселого. Рятує те, що битися доводиться не так часто. Хоча неуникних боїв з мутантами у квестах вистачає. Думаю, це можна було б виправити тим, щоб мутанти лякалися та відбігали після влучання, а не перли далі, як паротяг.
Про баги та оптимізацію… мені пощастило грати на RTX 4090 через GeForce Now. Та ніяких проблем зі швидкістю я не побачив. Щодо багів, то нічого визначного… окрім того, що в ланці головного квесту зіпсувалася послідовність та зникло “мирне рішення”, коли я вийшов “не тим шляхом” зі стартової зони — світ-то відкритий. Але далі все було добре. Втім, я спокійно ставлюся до персонажів, що палять у противогазі чи “стоять у дивані”.
Взагалі, я просто радий, що Сталкер 2 вийшов та ще й залишився автентичною грою, а не середнячком з лутом, крафтінгом, побудовою баз та деревом прокачування. Хотілося б ще такого ж майбутнього ставлення, як Cyberpunk 2077 отримав від CD Projekt Red. Але наразі святкуймо, бо є чому.
28.11.2024
Не знаю? Або не перевірив?
Безумовно, ознакою мудрості є здатність сказати “цього я не знаю”, а не вигадувати дурниць. Проте час від часу я стикаюсь з тим, що кажу “не знаю”, а насправді воно значить “мені не пригадалося це подивитися чи перевірити.” Та коли ти не на іспиті, має сенс тут зупинитись та дослідити потрібні джерела.
Інколи це значить написати запит до бази — так, може ми й не збираємо якусь метрику, але деколи її можна обчислити вручну. Та ми відразу оперуємо фактами, а не уявленнями.
Або робимо хак навколо чужої бібліотеки, бо вона невідомо як поводиться. Чи все ж залізти в її вихідний код та зʼясувати? Переважна більшість сучасного коду відкрита.
В мене сьогодні трапилось, що вже почав писати пояснення до ПРу з виправленням, але там не хотів писати “не знаю, чому, але воно працює”. Тоді розібрався, що ж таки відбувається в залежностях, та зрозумів, що виправлення моє було хибне, а треба було робити зовсім інше.
Чи є відоме явище, що поки складеш повноцінне питання до StackOverflow, то вже й сам розвʼязок знайдеш.
У фінансистів є належна обачність, а ми чим гірші?
27.11.2024
Keep-Alive та як воно працює
В мене тут намалювалася помилка, здається, через Keep-Alive. А саме, клієнт внутрішнього HTTP API в Go час від часу віддає помилку io.EOF
. Не найкраща помилка, якщо чесно, бо ніяк не пояснює, що відбулося. А відбулося ніби те, що сервер час від часу закриває підключення.
Keep-Alive (“залиш живим”) можна побачити у двох контекстах: TCP-сокетах та HTTP. Давайте спочатку про HTTP. Тут є заголовок Keep-Alive. Такий заголовок з боку клієнта каже “я б хотів використати це підключення більш ніж для одного запиту”. А з боку сервера, “так, я на це згодний”. На цьому участь HTTP в Keep-Alive закінчується. Ба більше, в HTTP/2 та далі цей заголовок взагалі заборонений! Бо в HTTP/2 сталість підключення є нормою. А в HTTP/3 втрачає сенс, оскільки він побудований на UDP, де передача даних відбувається взагалі без підключень.
Але що треба запамʼятати, то Keep-Alive на шарі HTTP тільки інформує, а справжня реалізація Keep-Alive відбувається на шарі TCP. Та для клієнтів API, гадаю, це все ще актуально, бо HTTP/3 з UDP не має сенсу. (А ви як думаєте?)
З TCP теж не все очевидно. Бо в підключення в принципі немає “терміну придатності”, воно буде існувати поки не закриєш. Але зайві підключення споживають ресурси сервера, тому сервер майже певно їх закриватиме. А щоб цього не відбулося, клієнт може час від часу надсилати так звані “посилки keepalive” (а насправді просто порожні посилки), щоб нагадати серверу, що підключення ще живе. (Сервер все одно може закрити таке підключення, але шанси на це менше.)
Для того в налаштуваннях підключення (в Go це в net.Dialer) є параметри Keepalive, які й вказують, чи надсилати такі пакети, та як часто. Я знав, що базовий клієнт Go вже підтримує стале підключення, а тепер я ще дізнався, що він також надсилає Keepalive, але надто рідко для цього серверу API. Отак.
26.11.2024
Несподівані речі з верстки для друку
Досить відомі (для мене) речі: розмір шрифту (обирається такий, щоб довжина рядка була близько 70 символів), відстань між рядками, відстань між параграфами, відступ першого рядка параграфа, поля, колонтитул з номером сторінки, стилі заголовків… все це мало відрізняється від вебу.
Розмір сторінки здивував, бо зазначений був як A5 (148x210 мм), але на практиці виявився 145x200. Але насправді треба зробити 149x204, щоб врахувати технічні поля на обрізку. А коли обкладинку робити, то ще цікавіше, бо там ще буде корінець, а товщина корінця залежить від кількості сторінок.
Ці дрібні зміни розміру, на недосвідчений погляд, нічого важливого не складають. Проте тут виявляється інший нюанс: на відміну від вебу, текст на сторінці є сталою величиною. В нього немає прокрутки та немає локальних обставин клієнта. Це, якщо хочете, вже векторне зображення, а не текст. З одного боку, тому верстка для друку простіша: її достатньо зробити один раз. З іншого боку, перед нами стоїть відповідальність все зробити правильно. Та навіть мінімальні зміни в полях чи інших загальних властивостях документа здатні попсувати всю верстку.
Що я маю на увазі. Обовʼязково стикнешся з ситуацією, де на сторінці “висітиме” один-два рядки. Або сторінка розріже важливий параграф чи список. Або заголовок не влізе в ширину рядка та одне слово перестрибне на наступний. Виправляти місцеві негаразди тексту — це теж верстка. Наприклад, можна на сторінці зменшити відстань між рядками — незначно та непомітно — і тоді на неї влізе останній рядок. Або зменшити розмір шрифту в заголовку. Або навпаки, розрядити текст, щоб він зайняв більше місця та виглядав охайніше.
Це і є для мене найбільшим здивуванням. Я звик думати про верстку (для вебу) як щось однорідне, впорядковане, де є ідеальна шкала шрифтів, вертикальний ритм тощо. А в книжці — звісно, для цього теж є місце — але ніякий вертикальний ритм не вартий висячого рядка. Хоча, я гадаю, можливі чи навіть існують (здивуйте мене ще раз) системи алгоритмічної верстки, які здатні виявити такі місця та внести потрібні корективи.
25.11.2024
Трасування зображень
Розкажу трохи більше про техніку трасування, яку я використовував вчора для карти. Трасування, або обведення — це спосіб перетворити растрове зображення в стилізоване векторне.
Взагалі нічого тут складного немає. Нам буде потрібний графічний редактор з шарами — Фотошоп, Pixelmator, GIMP, Paint.NET. Ставимо оригінал зображення на фон та починаємо обводити зверху всі потрібні деталі. Можна векторними інструментами (лініями, багатокутниками тощо), але можна й растровими — головне, щоб зміни були в окремих від фона шарах. Коли буде готово, вимикаємо фоновий шар та отримуємо чисте обведення. Звісно, якщо щось не подобається, можна завжди знову увімкнути фон та скоректувати.
Я цю техніку перший раз побачив на прикладі портрета, змальованого зі світлини. А ще чимало мультфільмів зроблені схожою технікою — називається ротоскопінг. Одним словом, підхід зі стажем.
Також трасують супутникові знімки, щоб створити векторну мапу. Наприклад. в редакторі OpenStreetMap кожен може спробувати (тільки, будь ласка, відповідально та обережно).
Ще я нещодавно, щоб зробити детальний план квартири (порожньої), сфотографував стіни, виміряв загальні розміри, а потім, переконавшись у дотриманні масштабу, переніс елементи зі світлини на план.
Багато графічних редакторів вміють робити трасування повністю автоматично. Але при тому вони відтворюють оригінал з усіма деталями. Інколи саме це нам й потрібно — наприклад, щоб потім збільшити це зображення. Натомість ручне обведення має сенс робити тоді, коли ми ще й хочемо прибрати зайві деталі або навіть вигадати нові. Наприклад, як на ілюстрації зверху.
24.11.2024
Ремастеринг книжки
Сьогодні цікавий день, бо я зголосився допомогти з сімейною книжкою. Книжка на 160 сторінок, стара, надрукована на аркушах A4 розворотом та зшита скріпками. Задача була просто скопіювати її, але наявний екземпляр сам був вицвілою ксерокопією, та мені свербіло її освіжити, або, як то тепер кажуть, зробити ремастер. Зацифрувати, зверстати, віддати в нормальну типографію.
Книжку мені дали в вигляді сканів тих самих розібраних аркушів. По-перше, потрібно було скласти з них сторінки в правильному порядку (бо на одному аркуші друкувалися сторінки 3 та 157, 4 та 156 тощо.) На то я зробив скрипт на Ruby із застосуванням ImageMagick:
left_index = index
right_index = 163 - index
angle = index.odd? ? 90 : -90
`magick '#{page}' -crop 100%x50% +repage output_%d.jpg`
`magick output_0.jpg -rotate #{angle} output_0.jpg`
`magick output_1.jpg -rotate #{angle} output_1.jpg`
FileUtils.move('output_0.jpg', "#{'%03d' % left_index}.jpg")
FileUtils.move('output_1.jpg', "#{'%03d' % right_index}.jpg")
Це дало мені впорядкований набір сторінок. Далі направив їх в ABBYY FineReader, яку памʼятаю ще зі шкільних часів. Якість розпізнавання відтоді стала незрівнянно краще, на виході отримуємо документ майже без помилок, а головне — з виправленими перенесеннями слів. Хоча вичитка все одно була потрібна, та виявилася складнішою, ніж я думав, бо перевірка граматики знаходила безліч хибних помилок у всіх назвах, іменах та місцевих словах. Так що робота була майже ручна.
Ще було питання, в якому редакторі робити чистку. Я почав у Google Docs, бо думав що це зараз найкращий вибір, але ні — зокрема там не вистачило банальної можливості вказати різні поля для лівої та правої сторінки. Тому більшість роботи виконав в Apple Pages. Не знаю, хто їм окрім мене користується, але насправді це повноцінний та потужний текстовий редактор. (Так, я знаю, що, мабуть, ідеальним вибором був би MS Word, але його в мене немає. Чи може Ворд зараз теж є онлайн?)
Нарешті, захотілося від себе додати локальну мапу. Але ж не просто знімок з Google Maps, а щось гідне книги. Трохи пошукав продукт, який вміє робити карти за потребою (наприклад, такий, де можна виділити потрібні мені селища.) А потім зрозумів — мені потрібно взяти знімок з карти та обмалювати його в графічному редакторі. Тоді можна наповнити карту будь-яким змістом, та при тому зберегти масштаб та пропорції. Так я й зробив в Pixelmator - стилі в мене трохи примітивні, думаю, якщо запастися гарними географічними стилями для ліній та багатокутників, вийшло б ще краще.
23.11.2024
Місце для Offline First
Чомусь у світі мало застосунків, які й повноцінно доступні офлайн, і мають автоматичну синхронізацію з сервером. Навіть великі серйозні продукти потребують доступу в інтернет, принаймні для запису. Синхронізація локальних копій з серверною — досі складна задача без загальних рішень. Я вже довго на це дивлюсь, та набуваю думки, що краще не думати, що в тебе воно вийде.
Розробка вебзастосунків псує людину: ти забуваєш, що можна робити застосунки, які взагалі не мають серверної частини. Але застосунків без серверної частини повно, та ми всі користуємося ними щодня. Навіть веброзробники з вебтехнологіями вільні взяти рішення на кшталт Electron та робити собі SPA без серверної частини. Також рішення з локальною базою буде простіше, ніж всяке задумане під синхронізацію, тож навіть Offline First варто брати тільки якщо вам дійсно потрібно щось синхронізувати з хмарою. Тому якщо хочете зробити щось однокористувацьке, вистачить SQLite, а то й JSON. Ваша продуктивність вам віддячить.
Головна проблема такого рішення — як і його головна сила — що дані залишаються на одному пристрої. Для веброзробника це теж ненормальний стан речей, але це може виявитися зовсім не проблемою — дивлячись на застосунок. Он, Apple Health досі не має ніякої синхронізації. Також, обмін файлами на цей час — розвʼязана задача, тому може нам ніякий не Offline First потрібний, а файли в спільній теці чи хмарному диску.
Коли ми працюємо з чужими чи спільними даними, потреба в доступі до інтернету більш виправдана, тож тут доцільніше мати класичний вебзастосунок з тонким клієнтом та зберіганням на сервері. Особливо тому, що тоді не доведеться розвʼязувати конфлікти між змінами різних користувачів.
А ось коли ми хочемо, щоб дані зберігалися в першу чергу локально, але зміни з одного пристрою зʼявлялися миттєво на іншому — тут вже доведеться шукати рішення “Offline First” - Firebase, Realm, CloudKit - та готуватися витрачати на нього зусилля: навіть коли ніби синхронізація є вбудованою, безплатною вона не буде.
22.11.2024
Теги в каналі — тепер й на сайті
Додав до сайту підтримку тегів. Нарешті можу скинути посилання на серію про основи безпеки в Інтернеті — проміж інших, одна з моїх улюблених. Також можна насолоджуватись хмарою тегів, хоча я тільки почав її наповнювати.
Трохи нюансів. Я хотів теги для каналу окремо від інших розділів сайту. Знайшов, як це роблять, на форумі. Якщо без деталей, то такі теги треба робити окремою таксономією, назвати її stendap/tags
- і тільки так, бо ця назва стає шляхом в URL. Але також тепер в метаданих поста теж треба писати "stendap/tags"
, що вже дуже дивно виглядає. Зате результат досягнений.
Хмару тегів в наш час легко зробити з CSS, як ось тут пояснюють. Тільки там кожному тегу призначається клас розміру, а я його обчислюю на ходу з кількості постів. Ще залишається нормалізувати її відносно максимальної, що звучить просто, але в шаблонах Go арифметики немає, а є тільки виклики функцій, та ще й в Польській нотації: font-size:{{add 1 (mul $pageCount $multiplier)}}em
.
Ну і ще придумав на сторінці тегу показувати повні пости, але щоб легше було зорієнтуватись, додати також табличку зі змістом. А табличка вже була готова, бо в статтях на сайті вона зʼявляється, коли достатньо заголовків. Тож залишалось тільки адаптувати.
21.11.2024
Розширена інформація після невдалого тесту в Go
Захотілося, щоб після невдалого інтеграційного тесту я бачив журнал сервісів з Docker Compose. Бо в мене на CI вже й так журнал друкується наприкінці збірки, але за тим загальним журналом складно знайти потрібне місце.
Перше питання стало — як взагалі в Go викликати код після тесту? Рішення в стандартній бібліотеці не знайшов, але ті тести виконуються пакетом testify/suite
, а в нього є метод TearDownTest(), проміж інших. Тож тут проблем немає.
…Далі, як дізнатися, що тест був невдалий? Я очікував якогось аргументу в колбеці, але ні. Насправді той обʼєкт *testing.T
, навколо якого обертаються тести, має метод t.Failed(). (Взагалі раджу його вивчити детальніше, бо там багато чого цікавого, наприклад, можливість отримати тимчасову директорію для одного тесту - t.TempDir()
.) А в testify/suite
, до речі, через s.T()
отримуємо той самий обʼєкт.
(PS: а ще в testify/suite
знайшов suite.HandleStats - якщо його оголосити, то цей метод буде викликаний наприкінці всього пакету зі статистикою про час виконання та результати тестів.)
Окей, тепер, як отримати логи? Спробував через програмний клієнт Docker… надто низькорівневий, бо він повертає для логи конкретного контейнера, та ще й потоком. А мені потрібні зведені логи всіх контейнерів в проєкті. Тому просто викликаю консольну команду docker compose
. Для того є зручний високорівневий тип exec.Cmd. Йому можна прямо сказати “спрямуй вивід команди в мій вивід” і все, готово.