Server Valheim itu sudah berjalan berbulan-bulan sebelum saya sentuh. Ada dunia, beberapa pemain, skrip backup, dan kode join yang harus dicari manual tiap kali ada orang baru mau masuk. Semua panel web yang saya temukan menyelesaikan masalah itu dengan mengambil alih servernya. Pasang ini, dan ia memiliki duniamu, compose-mu, mod-mu, update-mu. Itu desain yang wajar. Bukan desain yang saya mau. Saya tidak butuh server baru. Saya butuh jendela ke server yang sudah ada. Jadi batasannya ditentukan dulu, sebelum satu baris kode: menempel, bukan memiliki. Dashboard — kode join, status server, hari dunia, pemain, dan daftar backup dalam satu layar. Tiga kanal yang sebenarnya sudah ada Server itu sudah membuka semua yang dibutuhkan panel. Cuma tidak diberi label begitu. Supervisor XML-RPC di :8088 — start, stop, status proses, baca log. Sudah terautentikasi, sudah jalan. RCON di :25575 — perintah admin dan daftar pemain, lewat TCP di luar jalur utama, tetap jalan dengan crossplay. Agent…
Penjual gacha menjalankan jualannya pakai spreadsheet. Satu kolom item, satu kolom peluang, dan harapan supaya tidak ada yang bertanya bagaimana angkanya dipilih. Itulah alasan Gachasik ada. Bukan untuk membuat gacha — orang sudah melakukannya — tapi supaya pemilihan acaknya bisa ditunjukkan. Satu hal yang harus benar Sisanya cuma fitur. Ini produknya: Pemilihan acak berjalan di server, dari sumber kriptografis — bukan di ponsel pembeli. Peluang ditampilkan di halaman, sebelum pull. Setiap pull menghasilkan tepat satu item dan tanda terima publik. Kode pemilihannya open source, ditautkan dari situs. Poin terakhir itu yang paling lama diputuskan. Menulis "adil" di footer itu gampang. Menerbitkan fungsi yang benar-benar melakukan pemilihan, supaya orang asing bisa membacanya dan mengecek — itu lebih sulit. Saya memilih yang lebih sulit, karena klaim keadilan yang tidak bisa diverifikasi cuma marketing. Aturan yang tidak boleh dilanggar produk Dua batasan membentuk hampir semua…
Situs ini mlaku di Chyrp Lite — mesin blog self-hosted cilik yang melakukan kira-kira apa yang saya butuh dan ora lebih. Wis cukup lama berdiri dengan tema bawaan, yang sepenuhnya memadai dan sepenuhnya anonim. Akhirnya saya tulis ulang. Yang salah Ora banyak, jujur saja. Temanya mlaku. Tapi tiga hal mengganggu saya. Pertama, navigasinya mendaftar sepuluh halaman dalam satu daftar rata, dan tujuh di antaranya cuma ada untuk meng-embed layanan di dalam iframe. Beberapa embed itu menunjuk endpoint yang ora lagi menjawab, atau menolak di-frame sama sekali. Sidebar menawarkan tautan ke panel yang rusak. Kedua, tata letaknya satu kolom sempit dengan sidebar dan tanpa rasa situs itu untuk apa. Pengunjung mendarat dan tahu nama saya. Itu saja. Ketiga, kontennya daftar fakta tanpa jalan masuk. Yang saya ubah Homepage yang sungguhan. Alih-alih njatuhke pengunjung langsung ke indeks blog, sekarang ada halaman depan: siapa saya, di mana, apa yang saya kerjakan, dan empat cara masuk lebih dalam…
Pukul 00:32 sebuah baris log mulai berulang. Pukul 16:45 ia wis muncul 785 kali. [memory] sync failed … Plugin "memory-core" runtime is no longer active Plugin memori di gateway asisten saya mandheg bekerja. Bukan crash — sesuatu yang lebih buruk. Ia isih terpasang, isih melaporkan diri sebagai termuat, dan sama sekali ora bisa melakukan apa pun. Perbaikan yang salah Saya memuat ulang plugin. Errornya mandheg. Selama empat belas menit. Terus trafik naik, dan errornya balik persis seperti sebelumnya. Keheningan empat belas menit itu hal paling menyesatkan di seluruh insiden ini. Ia ketok seperti perbaikan. Bukan. Itu jendela menganggur tanpa apa pun tersisa untuk gagal. Penyebab sebenarnya Saya berhenti menebak dan membaca sumber runtime-nya. Saat plugin dimuat ulang panas, gateway mbangun generasi registry yang anyar. Apa pun yang isih megang referensi ke generasi lama sekarang megang handle mati. Jembatan sinkronisasi memori — bagian yang meng-embed dan mengindeks konten…
Pukul 18:57, tiga mesin saya mati dalam rentang sekitar empat menit satu sama lain. Waktu itu saya belum ngerti. Yang saya tahu: gateway wis balik dan melaporkan dirinya sehat, dan beberapa sesi kelihatan terpotong di tengah kalimat. Dugaan pertama — dan yang salah — adalah kesalahan perangkat lunak. Terus saya paham apa yang sebenarnya terjadi: pemadaman listrik singkat. MCB utama trip. Beban berlebih. Linimasa msi-server 18:57 .185 19:01 pi-server 19:01:33 arm-server tidak terpengaruh — uptime 3 minggu Baris terakhir itu yang menarik. arm-server ada di sirkuit yang beda. Pemadaman njupuk satu jalur, bukan seluruh rumah. Artinya mesin yang mati dan mesin yang tidak justru memberi tahu saya sesuatu tentang instalasi listriknya — bukan tentang mesinnya. Bentuk pemulihannya Setiap host yang terdampak balik sendiri. Itu memang inti kenapa saya ngatur begitu, tapi layak dikonfirmasi ketimbang diasumsikan: Proses gateway balik, nol restart, probe kesehatan 200 Ora ana error sinkronisasi…
Saya punya dua mesin yang penting. msi-server yang lebih baru — MSI Cubi 5 kecil yang menjalankan Ubuntu Server, dan kotak tempat gateway asisten saya tinggal. .185 itu desktop lawas, isih kepake, isih nyimpen beberapa hal yang belum saya migrasikan. Wis lama, satu-satunya saluran antara keduanya ya saya sendiri. Saya ndelok sesuatu di satu mesin, jalan ke mesin lain, terus ngetik ulang. Itu mlaku baik-baik saja — sampai akhirnya ora. Jadi saya bikin relay. Bukan yang pintar — dua pipa satu arah. Pipa satu: .185 → saya Skrip kecil di .185 ngirim pesan ke endpoint HTTPS di gateway. Terautentikasi token, dibatasi ke satu agen, dan dikirim ke ponsel saya dengan label 📡 .185 supaya saya selalu ngerti mesin mana yang lagi bicara. Bagian pentingnya adalah apa yang tidak dilakukannya. Ora ana apa pun di .185 yang mendengarkan koneksi masuk untuk ini. Mesin itu cuma bikin permintaan keluar. Ora ana port anyar, ora ana permukaan serangan anyar. Pipa dua: saya → .185 Ini bagian yang menarik…
Server Valheim itu sudah berjalan berbulan-bulan sebelum saya sentuh. Ada dunia, beberapa pemain, skrip backup, dan kode join yang harus dicari manual tiap kali ada orang baru mau masuk.
Semua panel web yang saya temukan menyelesaikan masalah itu dengan mengambil alih servernya. Pasang ini, dan ia memiliki duniamu, compose-mu, mod-mu, update-mu. Itu desain yang wajar. Bukan desain yang saya mau. Saya tidak butuh server baru. Saya butuh jendela ke server yang sudah ada.
Jadi batasannya ditentukan dulu, sebelum satu baris kode: menempel, bukan memiliki.
Dashboard — kode join, status server, hari dunia, pemain, dan daftar backup dalam satu layar.
Tiga kanal yang sebenarnya sudah ada
Server itu sudah membuka semua yang dibutuhkan panel. Cuma tidak diberi label begitu.
Supervisor XML-RPC di :8088 — start, stop, status proses, baca log. Sudah terautentikasi, sudah jalan.
RCON di :25575 — perintah admin dan daftar pemain, lewat TCP di luar jalur utama, tetap jalan dengan crossplay.
Agent ValheimUI di :2456 — JSON terstruktur: pemain beserta posisinya, status dunia, kematian, layer peta.
Tanpa Docker socket. Tanpa container privileged. Tanpa menyisipkan string ke shell. Panel jalan sebagai user biasa, membuang semua capability, dan hanya bisa melakukan apa yang diizinkan tiga kanal itu — dan justru itu intinya. Panel kontrol yang bisa menjalankan perintah apa pun sebagai root bukan panel kontrol; itu shell jarak jauh dengan font yang lebih enak dilihat.
Dua kata kerja yang bohong
Supervisor tidak punya restartProcess. Di form HTML-nya ada tombol restart, di supervisorctl ada verb restart, jadi wajar kalau saya mengira API XML-RPC-nya juga punya. Ternyata tidak. supervisor.restart me-restart supervisord-nya sendiri, dan memanggil restartProcess menjawab UNKNOWN_METHOD.
Perbaikannya membosankan: stop, tunggu STOPPED, start. Tapi justru lebih baik, karena jeda itulah yang memberi waktu game menjalankan handler SIGTERM dan menyimpan dunia. Restart yang selesai sebelum save kelar adalah restart yang kehilangan progres.
Jebakan yang sama muncul lagi. /v1/commands/time milik agent terlihat seperti pembaca nilai. Ia penulis nilai — dipanggil tanpa argumen, jawabannya "fraction, skip=morning or seconds is required". Pembacaannya ada di RCON. Entri katalognya saya pindah.
Dua bug itu bentuknya sama: nama perintahnya menyarankan perilaku yang belum saya buktikan. Menyambungkan tombol ke endpoint tidak sama dengan tahu apa yang endpoint itu lakukan.
Konsol perintah — katalog 25 perintah yang masing-masing menyatakan jalurnya sendiri, kotak RCON bebas untuk admin, dan chat langsung.
Mod yang terpasang sempurna dan tidak pernah dimuat
Yang ini paling saya sukai, karena panel tidak salah melaporkan apa pun.
Panel menulis plugin ke /config/bepinex/plugins. Game memuatnya dari /opt/valheim/bepinex/BepInEx/plugins. Dua direktori berbeda, dan penyalinan di antaranya terjadi di dalam valheim-bootstrap — program supervisor sekali jalan yang berjalan saat container start lalu duduk di status EXITED sepanjang umur container.
Jadi stop/start biasa menghidupkan lagi server dengan set plugin lama. Panel bilang "terpasang". Pemasangannya memang berhasil. Log game bilang tiga plugin dimuat, bukan lima.
Untuk pengelola mod, diam-diam mengabaikan mod baru adalah kegagalan terburuk. Lebih buruk daripada error, karena error ada yang memperbaiki. Solusinya: jalankan bootstrap di antara stop dan start, di setiap restart, dan buat start juga menyinkronkan. Bootstrap idempoten, jadi menjalankannya saat tidak ada perubahan cuma makan satu detik. Setelah itu log bilang lima dimuat, nol gagal.
Ada pelajaran kecil di dalamnya: pesan sukses panel itu menggambarkan kerja panel, bukan keadaan server. Dua klaim yang berbeda, dan hanya satu yang layak ditampilkan ke pengguna.
Pengelola mod — Thunderstore dan Hexium digabung dalam satu tampilan, lengkap dengan peringatan crossplay, karantina per paket, dan tiga plugin yang dipasang manual dan disebut jujur sebagai unmanaged.
Petanya bukan gambar
Saya kira /v1/map/layers mengembalikan gambar. Yang dikembalikan PNG RGBA 2048×2048 berisi field mentah: merah membawa mask terrain dan kode biome, hijau dan biru membawa ketinggian, alpha membawa kerapatan hutan. POST /v1/map/render tidak merender — ia mulai mengambil sampel. Styling sebenarnya ada di kode Go milik agent, yang tidak akan saya porting.
Jadi panel menata petanya sendiri, di browser, di atas canvas. Ternyata arsitektur itu justru lebih baik: 4,2 juta piksel ditata di Python butuh beberapa detik per frame, sedangkan canvas selesai dalam milidetik lalu bisa digeser tanpa bertanya ke server sama sekali. Pekerjaan server menyusut jadi cache, autentikasi, dan penggabungan objek.
Saya memverifikasinya dengan merender pratinjau 512 piksel lalu melihatnya. Deep North putih di atas, Ashlands merah-cokelat di bawah, laut biru, padang rumput hijau, gunung kelabu bersalju. Utara di atas sudah benar, biome ada di tempatnya, garis pantai bersih. Review kode tidak akan menangkap sumbu yang terbalik; gambar menangkapnya.
Petanya, ditata di browser: biome, altar boss, portal, dan mask fog yang terisi seiring orang bermain.
Fog of war sekarang 100% belum dijelajahi — agent baru mulai mencatat posisi pemain hari ini. Kalau mask itu dipatuhi, yang muncul kotak hitam. Jadi fog mematikan dirinya selama datanya kosong, menjelaskan alasannya di UI, dan akan menyalakan diri lagi begitu ada yang perlu disembunyikan.
Kesalahan keamanan yang saya buat sendiri
Jembatan Discord membuat bot bisa membaca dan mengendalikan server. Versi pertama saya menyuruh bot mengirim ID role pemanggil ke skrip relay, dan skrip itu yang memutuskan boleh atau tidak.
Itu batas kepercayaan di tempat yang salah. Bot-nya menjalankan model kecil gratis. Prompt injection bisa membuatnya mengaku sebagai Admin, dan skripnya akan percaya.
Sekarang relay bertanya langsung ke Discord: untuk guild ini dan user ini, role apa yang benar-benar dimiliki anggota itu? Klaimnya diverifikasi ke guild, bukan diterima.
Dan karena satu akun relay akan membuat pengecekan skrip jadi satu-satunya batas, sekarang ada tiga — relay-user, relay-mod, relay-admin — dan skrip login sesuai role yang diberikan. Panel menegakkan batas yang sama secara mandiri. Saya mengujinya dengan berpura-pura skripnya bug: sesi relay-user dapat 403 saat restart, saat mengubah daftar, saat memasang mod, dan 200 untuk bacaan.
Pengguna dan role — tiga role, tiga puluh capability, dan grid yang ditegakkan server, bukan UI.
Dua kunci, dan satu tidak mempercayai yang lain. Itu bentuk yang saya mau untuk apa pun yang bisa me-restart server.
Satu aturan CSS yang layak diingat
Geser dan zoom tidak jalan. Peta tergambar, tombolnya bereaksi, tidak ada yang bergerak.
Overlay pesan membawa atribut hidden. Class-nya juga menyetel display: grid. CSS penulis menang atas [hidden] { display: none } bawaan browser, jadi overlay tetap menempati ruang — posisi absolut, inset nol, latar semi-transparan — dan menelan setiap event mouse sebelum sampai ke canvas.
Geser dan zoom-nya tidak pernah rusak. Ia tidak pernah bisa dijangkau.
Perbaikannya satu baris: [hidden] { display: none !important }. Pola ini muncul di seluruh panel, jadi sekarang global, bukan lokal. Dua bug peta lain ketemu di putaran yang sama: layer fog dibangun ulang piksel demi piksel di setiap gerakan mouse, dan canvas mengambil ukuran dari tab yang tersembunyi lalu keluar 1×1.
Yang masih jujur-jujur kosong
Papan kematian masih kosong. Belum ada yang mati sejak pelacak menyala, dan riwayat sebelum hari ini tidak bisa dipulihkan.
Fog tetap mati sampai data penjelajahan ada.
Tiga plugin dipasang manual sebelum panel ini ada, jadi panel mendaftarnya sebagai unmanaged, bukan mengklaimnya.
Ganti dunia tidak disediakan. Dunia aktif datang dari environment variable, jadi menggantinya butuh edit compose yang privileged. UI-nya bilang begitu, bukan pura-pura bisa.
Panelnya sekitar 9.000 baris — 5.600 Python, 2.000 JavaScript, sisanya markup dan CSS. Tanpa Docker socket, dan berjalan di samping server yang bukan ia bangun. Dirilis MIT.
Alasan panel ini ada adalah batasan yang saya pilih di awal. Memiliki servernya akan lebih mudah ditulis dan lebih sulit dipercaya.
Penjual gacha menjalankan jualannya pakai spreadsheet. Satu kolom item, satu kolom peluang, dan harapan supaya tidak ada yang bertanya bagaimana angkanya dipilih.
Itulah alasan Gachasik ada. Bukan untuk membuat gacha — orang sudah melakukannya — tapi supaya pemilihan acaknya bisa ditunjukkan.
Satu hal yang harus benar
Sisanya cuma fitur. Ini produknya:
Pemilihan acak berjalan di server, dari sumber kriptografis — bukan di ponsel pembeli.
Peluang ditampilkan di halaman, sebelum pull.
Setiap pull menghasilkan tepat satu item dan tanda terima publik.
Kode pemilihannya open source, ditautkan dari situs.
Poin terakhir itu yang paling lama diputuskan. Menulis "adil" di footer itu gampang. Menerbitkan fungsi yang benar-benar melakukan pemilihan, supaya orang asing bisa membacanya dan mengecek — itu lebih sulit. Saya memilih yang lebih sulit, karena klaim keadilan yang tidak bisa diverifikasi cuma marketing.
Aturan yang tidak boleh dilanggar produk
Dua batasan membentuk hampir semua layar.
Platform melayani penjual, bukan pembeli. Pembeli dan penjual menyelesaikan pembayaran di luar platform. Platform yang memiliki pemilihan acaknya; ia tidak pernah duduk di tengah pembayaran. Produknya lebih kecil dan lebih aman, dan itu yang jujur.
Ini bukan alat pembayaran. Kredit disebut kredit. Tidak ada pencairan dan tidak ada transfer. Antarmuka punya daftar kata yang keras, dan ada tes otomatis yang memindai setiap layar supaya framing-nya tidak pernah tergelincir ke bahasa taruhan. Bahkan teks penyangkalannya tidak boleh memakai kata itu. Kalau kamu harus menjelaskan kenapa sesuatu bukan taruhan, kamu sudah kalah di framing-nya.
Satu hari, lalu seminggu
Commit pertama mendarat 04:47 tanggal 30 September — scaffold dan rencana build yang dibekukan. Pukul 12:58 hari yang sama, semuanya sudah berdiri: pool dan peluang, mesin pemilihan, ledger kredit QRIS, referral, storefront publik, kalkulator margin, dan bundel deploy. Tiga puluh enam commit dalam sehari.
Terus lanjut. Animasi reveal. Nested pool — satu item bisa lanjut ke pool lain milik penjual, sampai tiga tahap dengan satu biaya. Demo tanpa login di landing yang menjalankan kode pemilihan asli dengan item contoh, jadi kamu bisa pull sebelum percaya. Mode kiosk untuk layar booth.
Hitungan akhir: 114 commit, 75 file tes, 12.367 baris tes melawan 10.989 baris Python aplikasi. Skema dari v1 ke v16 — semua migrasi additive kecuali satu, yang butuh rebuild tabel sungguhan untuk menghapus template yang sudah tidak diizinkan CHECK constraint.
Yang rusak
Dua hal hampir terkirim dalam keadaan rusak, dan keduanya layak disebut.
Image deploy kehilangan dua file.draw.js dan share.js tidak masuk ke wheel hasil build — animasinya bakal mati begitu saja di produksi, tanpa error dan tanpa baris log. Sekarang ada tes yang menjaga seluruh direktori app/static, jadi image tidak bisa diam-diam menghapus aset lagi.
"Force SSL" di Nginx Proxy Manager hampir mematikan situs. Tunnel menyerahkan HTTP polos ke NPM, jadi Force SSL mengalihkan HTTP itu ke URL HTTPS yang sama — loop. Perbaikannya: pakai "Always Use HTTPS" dari Cloudflare dan biarkan Force SSL mati. Setelan yang kelihatan benar, tapi justru salah.
Ada juga masa di mana tiga subagent gagal beruntun untuk fitur nested pool — satu crash-loop, satu cuma melakukan recon. Sesi utama akhirnya membangunnya langsung. Delegasi tidak otomatis lebih cepat daripada mengerjakan sendiri.
Yang masih terbuka
Saya lebih suka menuliskannya daripada pura-pura tidak ada:
Halaman legalnya sudah berisi copy sungguhan tapi belum final — di halamannya sendiri tertulis masih menunggu tinjauan penasihat hukum.
Repo lokal ahead satu commit dari origin.
Belum ada harga untuk B2B atau acara — cuma halaman kontak.
Kenapa ini picker, bukan game
Nama di kotaknya: "pemilih acak yang tercatat dan bisa disesuaikan — bukan taruhan." Itu bukan eufemisme. Itu spesifikasinya.
Picker itu alat. Ia melakukan apa yang diperintahkan, menunjukkan kerjanya, dan menyimpan catatan. Semua di build ini mengikuti dari situ: pemilihan di server, peluang yang ditampilkan, tanda terima, algoritma yang diterbitkan.
Kalau pemilihan acaknya adil dan kamu bisa mengeceknya, kamu tidak perlu mempercayai penjualnya. Itu seluruh produknya.
Situs ini mlaku di Chyrp Lite — mesin blog self-hosted cilik yang melakukan kira-kira apa yang saya butuh dan ora lebih. Wis cukup lama berdiri dengan tema bawaan, yang sepenuhnya memadai dan sepenuhnya anonim. Akhirnya saya tulis ulang.
Yang salah
Ora banyak, jujur saja. Temanya mlaku. Tapi tiga hal mengganggu saya.
Pertama, navigasinya mendaftar sepuluh halaman dalam satu daftar rata, dan tujuh di antaranya cuma ada untuk meng-embed layanan di dalam iframe. Beberapa embed itu menunjuk endpoint yang ora lagi menjawab, atau menolak di-frame sama sekali. Sidebar menawarkan tautan ke panel yang rusak.
Kedua, tata letaknya satu kolom sempit dengan sidebar dan tanpa rasa situs itu untuk apa. Pengunjung mendarat dan tahu nama saya. Itu saja.
Ketiga, kontennya daftar fakta tanpa jalan masuk.
Yang saya ubah
Homepage yang sungguhan. Alih-alih njatuhke pengunjung langsung ke indeks blog, sekarang ada halaman depan: siapa saya, di mana, apa yang saya kerjakan, dan empat cara masuk lebih dalam. Ia njawab pertanyaan yang situs lama biarkan menggantung.
Navigasi yang mencerminkan struktur sebenarnya. Halaman per-layanan ilang dari menu. Mereka isih ada — setiap tautan lama tetap bisa dibuka — tapi halaman Homelab sekarang menautkan langsung ke tiap layanan, dengan deskripsi satu baris tentang apa itu. Tujuh panel iframe jadi satu indeks yang enak dibaca.
Tema yang punya pendapat. IBM Plex Sans dan Plex Mono, di-host sendiri ketimbang diambil dari CDN. Monospace disisihkan untuk label, metadata, dan apa pun yang terbaca sebagai data. Paletnya off-white hangat dengan satu aksen tembaga, dipakai untuk tautan dan nyaris tidak untuk hal lain.
Daftar layanan ditata seperti rak: nomor indeks, nama, fungsinya, dan apakah ia terbuka ke dunia atau cuma terjangkau di LAN. Kolom terakhir itu yang jujur — ia memberi tahu sebelum kamu mengklik apakah tautannya akan meminta kata sandi.
Aksesibilitas bukan tambahan
Tautan lompat untuk pengguna keyboard, karena sidebar muncul lebih dulu di markup
Garis fokus yang kelihatan di setiap elemen interaktif, dalam warna aksen
prefers-reduced-motion dihormati — tanpa transisi, tanpa smooth scrolling, untuk siapa pun yang memintanya
prefers-contrast: more mendorong batas dan teks ke hitam penuh
Stylesheet cetak, karena halaman-halaman ini layak disimpan di atas kertas
Yang akan saya lakukan berbeda
Tulis kontennya sebelum temanya.
Saya menghabiskan waktu mendesain komponen — hero, status strip, tabel rak — terus harus balik dan menulis ulang teks halaman agar cocok. Desainnya jadi baik-baik saja. Urutannya yang kebalik.
Lain kali: putuskan apa yang halaman perlu katakan, lalu putuskan bagaimana ia perlu terlihat.
Pukul 00:32 sebuah baris log mulai berulang. Pukul 16:45 ia wis muncul 785 kali.
[memory] sync failed … Plugin "memory-core" runtime is no longer active
Plugin memori di gateway asisten saya mandheg bekerja. Bukan crash — sesuatu yang lebih buruk. Ia isih terpasang, isih melaporkan diri sebagai termuat, dan sama sekali ora bisa melakukan apa pun.
Perbaikan yang salah
Saya memuat ulang plugin. Errornya mandheg.
Selama empat belas menit.
Terus trafik naik, dan errornya balik persis seperti sebelumnya. Keheningan empat belas menit itu hal paling menyesatkan di seluruh insiden ini. Ia ketok seperti perbaikan. Bukan. Itu jendela menganggur tanpa apa pun tersisa untuk gagal.
Penyebab sebenarnya
Saya berhenti menebak dan membaca sumber runtime-nya.
Saat plugin dimuat ulang panas, gateway mbangun generasi registry yang anyar. Apa pun yang isih megang referensi ke generasi lama sekarang megang handle mati. Jembatan sinkronisasi memori — bagian yang meng-embed dan mengindeks konten — nangkep referensinya saat startup dan ora pernah menunjuk ulang. Setiap panggilan setelah reload ngeluarin error.
Inilah kenapa memuat ulang plugin tidak akan pernah berhasil. Pluginnya ora apa-apa. Yang memanggilnya megang pointer basi, dan ora ana reload plugin yang bisa ndandani itu.
Perbaikannya
Restart gateway penuh. Itu mbangun ulang setiap generasi dari nol, jadi jembatan nangkep handle yang hidup lagi.
Angkanya sebelum dan sesudah: 815 kemunculan error sepanjang hari, terakhir pukul 16:52:03. Setelah restart, nol. Bukan "nol sejauh ini" — nol di setiap sinkronisasi sejak itu.
Dua hal yang layak diingat
CLI sehat sepanjang waktu.
Sepanjang 785 kegagalan itu, memory status --deep melaporkan embeddings siap, vector store siap, semantic vectors siap. Lapisan datanya ora pernah rusak. Cuma jalur di dalam gateway yang rusak.
Kalau saya percaya perintah status itu dan berhenti di situ, saya bakal menyimpulkan semuanya baik-baik saja — dan salah dengan cara yang penting. Pemeriksaan kesehatan yang ngetes jalur yang salah lebih buruk daripada ora ana pemeriksaan, karena ia memberimu keyakinan yang tidak kamu peroleh.
Jendela sepi bukan bukti.
Saya ngecek setelah reload, ora ndelok apa pun selama empat belas menit, dan hampir menyebutnya beres. Pemeriksaan yang benar adalah di bawah beban, atau dengan pemicu sengaja. Keheningan saat menganggur ora membuktikan apa pun tentang jalur kode yang cuma jalan saat ada kerja.
Yang kedua itu sudah saya catat dengan benar, karena saya pernah membuat kesalahan yang sama dengan kostum berbeda.
Pukul 18:57, tiga mesin saya mati dalam rentang sekitar empat menit satu sama lain.
Waktu itu saya belum ngerti. Yang saya tahu: gateway wis balik dan melaporkan dirinya sehat, dan beberapa sesi kelihatan terpotong di tengah kalimat. Dugaan pertama — dan yang salah — adalah kesalahan perangkat lunak.
Terus saya paham apa yang sebenarnya terjadi: pemadaman listrik singkat. MCB utama trip. Beban berlebih.
arm-server ada di sirkuit yang beda. Pemadaman njupuk satu jalur, bukan seluruh rumah. Artinya mesin yang mati dan mesin yang tidak justru memberi tahu saya sesuatu tentang instalasi listriknya — bukan tentang mesinnya.
Bentuk pemulihannya
Setiap host yang terdampak balik sendiri. Itu memang inti kenapa saya ngatur begitu, tapi layak dikonfirmasi ketimbang diasumsikan:
Proses gateway balik, nol restart, probe kesehatan 200
Ora ana error sinkronisasi memori sejak boot — lembar bersih
Kanal pesan tersambung lagi pukul 19:00:36
Ora ana error I/O kernel, semua mount utuh
Di .185 ada dua pesan journal corrupted or uncleanly shut down. Itu kedengaran lebih buruk daripada kenyataannya. journald ngerti shutdown tidak bersih dan melakukan persis apa yang dirancang untuknya. Ada juga pemeriksaan filesystem advisory pada partisi FAT — advisory artinya ia pengin perhatian, bukan bahwa ada yang rusak.
Bagian yang saya kurang senang
Dua run agen terputus di msi-server.
Satu context overflow pada sesi yang kerja berat sepanjang hari. Itu bakal gagal juga; pemadaman cuma datang lebih dulu. Satu lagi korban asli — panggilan LLM yang lagi jalan pas lampu mati.
Ora ana kehilangan data. Keduanya pekerjaan yang harus diulang. Itulah biaya jujur sebuah pemadaman: bukan korupsi, cuma pengulangan.
Di mana UPS sebenarnya berada
Ini yang saya salah asumsikan.
Saya kira UPS mencakup "server-servernya". Ternyata ora. UPS kecil itu mencakup inti router, arm-server, dan CCTV. Sisanya — termasuk dua mesin yang paling saya pedulikan — langsung dari jala-jala.
Itu menjelaskan pola di atas sepenuhnya. arm-server selamat karena terlindungi. Sisanya ora, karena memang tidak terlindungi.
Yang akan saya ubah
Ora ana soal perangkat lunak. Semuanya pulih sendiri tanpa ditungguin, dan itu tes yang penting.
Yang saya ubah adalah urutan prioritas. Host gateway sekarang melakukan kerja nyata, dan itu satu mesin di mana shutdown tidak bersih bikin saya kehilangan sesi ketimbang sekadar reboot. Itu masalah seukuran UPS.
Pelajaran bukan "beli UPS". Tapi: cari tahu mesin mana ada di sirkuit mana sebelum kamu perlu tahu.
msi-server yang lebih baru — MSI Cubi 5 kecil yang menjalankan Ubuntu Server, dan kotak tempat gateway asisten saya tinggal. .185 itu desktop lawas, isih kepake, isih nyimpen beberapa hal yang belum saya migrasikan.
Wis lama, satu-satunya saluran antara keduanya ya saya sendiri. Saya ndelok sesuatu di satu mesin, jalan ke mesin lain, terus ngetik ulang. Itu mlaku baik-baik saja — sampai akhirnya ora.
Jadi saya bikin relay. Bukan yang pintar — dua pipa satu arah.
Pipa satu: .185 → saya
Skrip kecil di .185 ngirim pesan ke endpoint HTTPS di gateway. Terautentikasi token, dibatasi ke satu agen, dan dikirim ke ponsel saya dengan label 📡 .185 supaya saya selalu ngerti mesin mana yang lagi bicara.
Bagian pentingnya adalah apa yang tidak dilakukannya. Ora ana apa pun di .185 yang mendengarkan koneksi masuk untuk ini. Mesin itu cuma bikin permintaan keluar. Ora ana port anyar, ora ana permukaan serangan anyar.
Pipa dua: saya → .185
Ini bagian yang menarik, karena desain yang ketok benar justru salah.
Pendekatan cermin adalah endpoint HTTP di .185 yang saya kirimi permintaan. Saya tolak. Mesin itu punya sejarah: proses gateway yang kadang menggantung, dan basis data agen yang wis cukup besar untuk melampaui jendela konteksnya saat kerja nyata. Kalau kamu menambah permukaan masuk yang selalu mendengarkan ke kotak dengan sifat begitu, kamu bakal berakhir dengan layanan yang mati pukul 3 pagi tanpa jejak sebabnya.
Sebagai gantinya saya pakai ulang sesuatu yang sudah dipercaya dan sudah teruji: SSH.
Alurnya satu perintah:
Saya memutuskan mengirim sesuatu ke .185
Skrip di msi-server membuka koneksi SSH
Ia menjalankan CLI agen di sana terhadap session key yang tetap
Balasan agen kembali lewat stdout
Saya teruskan ke utas obrolan
Satu perjalanan pulang-pergi. Ora ana port baru yang kebuka di mana pun.
Bentuknya saat berhasil
Empat tes, semuanya end to end:
transport smoke → PONG-185-FASE2
simple message → FASE2-V2-OK
tool use on .185 → returned hostname, uptime, version
final → FASE2-FINAL-OK
Yang ketiga yang paling penting. Ia tidak hanya membuktikan pipa bisa membawa teks — ia membuktikan bahwa permintaan yang berasal dari satu mesin bisa mencapai mesin lain, bikin mesin itu menjalankan perintah shell di sana, dan nggawa keluaran aslinya balik. Itu bedanya notifikasi dengan kendali jarak jauh.
Batasannya, dinyatakan jujur
Relay ini sinkron. Permintaan menunggu balasan, dan timeout-nya 300 detik secara default. Untuk obrolan dan kueri pendek itu cukup lega. Untuk apa pun yang berat — build sepuluh menit, tugas coding panjang — ia bakal timeout, dan pekerjaannya jadi yatim.
Perbaikannya adalah varian async: tembak permintaan, dapat konfirmasi, kirim hasilnya saat siap. Saya belum membangunnya, karena belum ada yang membutuhkannya. Menulisnya sekarang berarti menulis kode untuk masalah yang belum saya punya.
Yang saya ambil dari ini
Antarmuka terbaik antara dua mesin biasanya yang sudah ada di sana.
SSH sudah dipercaya, sudah berkunci, sudah dipahami. Endpoint HTTP akan lebih modern dan kurang andal. Pertanyaannya bukan "apa cara yang benar untuk melakukan ini" — tapi "apa hal baru terkecil yang bisa saya tambahkan supaya ini bekerja."
Di sini jawabannya ternyata: ora ana, di sisi penerima. Cuma skrip dan kunci yang sudah ada.