[{"data":1,"prerenderedAt":941},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:en":3,"api:\u002Fapi\u002Fengineering:en":8},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","en","Ewsirdev","Web developer: websites, landing pages and Go services",[9,98,175,244,298,348,393,478,552,629,705,790,863],{"slug":10,"title":11,"stack":12,"summary":19,"nodes":20,"edges":49,"flows":63,"decisions":71,"tradeoffs":84,"tests":87,"source":88,"stats":89},"job-queue","Conveyor: job queue",[13,14,15,16,17,18],"Go","SQLite","REST API","SSE","Prometheus","Docker","A background job queue on SQLite: priorities, retries with backoff, cron, idempotency keys. The point is the guarantees: at-least-once, lease and fencing.",[21,27,31,36,39,42,46],{"id":22,"label":23,"row":24,"col":24,"span":25,"kind":26},"prod","API client\nPOST \u002Fapi\u002Fjobs",0,1,"client",{"id":28,"label":29,"row":24,"col":25,"span":25,"kind":30},"cron","Scheduler\ncron schedules","core",{"id":32,"label":33,"row":25,"col":24,"span":34,"kind":35},"db","SQLite (WAL)\njobs · runs · schedules",2,"store",{"id":37,"label":38,"row":34,"col":24,"span":25,"kind":30},"claim","Claim\nUPDATE … RETURNING",{"id":40,"label":41,"row":34,"col":25,"span":25,"kind":30},"reaper","Reaper\nexpired leases",{"id":43,"label":44,"row":45,"col":24,"span":25,"kind":30},"pool","Worker pool\nlease heartbeat",3,{"id":47,"label":48,"row":45,"col":25,"span":25,"kind":26},"dash","Dashboard and \u002Fmetrics\nSSE · Prometheus",[50,51,52,53,55,58,60],{"n":25,"from":22,"to":32},{"n":34,"from":28,"to":32},{"n":45,"from":32,"to":37},{"n":54,"from":37,"to":43},4,{"n":56,"from":43,"to":32,"side":57},5,"L",{"n":59,"from":40,"to":32},6,{"n":61,"from":32,"to":47,"side":62},7,"R",[64,65,66,67,68,69,70],"A job is enqueued over REST with a Bearer token; enqueueing again with the same idempotency key returns the existing job.","Each cron run is enqueued with the key cron:\u003Cid>:\u003Ctime> and next_run is moved by a conditional UPDATE, so several processes cannot create a duplicate.","The highest-priority ready job is picked by priority DESC, run_at, id.","A single statement sets the status, attempt counter, lease deadline and a unique lease token, and hands the job to a worker.","The worker extends the lease with a heartbeat every third of the term and finishes the job (Complete\u002FFail) presenting the token: a zombie worker gets ErrLeaseLost.","The reaper returns jobs with an expired lease to the queue with backoff, or to dead when attempts are exhausted.","A state snapshot goes to the dashboard over SSE once a second; counters and a histogram are exposed on \u002Fmetrics.",[72,75,78,81],{"title":73,"text":74},"Atomic claim with one UPDATE … RETURNING","Picking the job and granting the lease happen in a single statement, so two workers, even from different processes on the same database file, cannot get the same job.",{"title":76,"text":77},"Lease, heartbeat and fencing token","A crashed worker does not lose the job: the lease expires, and Complete\u002FFail\u002FExtend check the token, so a zombie that wakes up cannot corrupt the result.",{"title":79,"text":80},"At-least-once, not exactly-once","A handler may be called again, so handlers must be idempotent. The queue does not promise what it cannot guarantee.",{"title":82,"text":83},"Backoff with jitter","The delay is BACKOFF_BASE · 2^(n-1) with a cap, multiplied by a random factor of 0.5–1; a Permanent error or an unknown type goes straight to dead.",[85,86],"A single SQLite file: scales only on one machine (several processes on one volume are fine, over the network they are not).","Metric counters live in memory and reset on restart; there are no job chains, per-queue rate limits or roles (one token).","Concurrent claiming (400 jobs, two \"processes\" on one file, 12 workers: no double execution), lease expiry and fencing, racing idempotency keys, cron from two processes, graceful shutdown.","README of the job-queue project (in Russian), sections \"Архитектура\" (Architecture), \"Гарантии доставки\" (Delivery guarantees), \"Ограничения\" (Limitations)",{"slug":10,"kind":90,"go_lines":91,"go_test_lines":92,"tests":93,"endpoints":94,"html":95,"css":96,"js":97},"go",2161,833,25,19,138,188,331,{"slug":99,"title":100,"stack":101,"summary":102,"nodes":103,"edges":127,"flows":139,"decisions":149,"tradeoffs":162,"tests":165,"source":166,"stats":167},"uptime-monitor","Pulse: uptime monitor",[13,14,16,15,18],"An uptime monitor: HTTP and TCP checks, uptime, p50\u002Fp95, incidents, a live dashboard and a public status page.",[104,107,110,113,116,118,121,124],{"id":105,"label":106,"row":24,"col":24,"span":25,"kind":30},"mgr","Manager\none goroutine per check",{"id":108,"label":109,"row":24,"col":25,"span":25,"kind":26},"target","Targets\nHTTP \u002F TCP",{"id":111,"label":112,"row":25,"col":24,"span":25,"kind":30},"run","Run\ncheck + timing",{"id":114,"label":115,"row":25,"col":25,"span":25,"kind":30},"hub","Hub (pub\u002Fsub)\nnon-blocking",{"id":35,"label":117,"row":34,"col":24,"span":34,"kind":35},"SQLite (WAL)\nchecks · results · incidents",{"id":119,"label":120,"row":45,"col":24,"span":25,"kind":30},"stats","stats\np50\u002Fp95 · uptime",{"id":122,"label":123,"row":45,"col":25,"span":25,"kind":30},"det","Detector\nincidents",{"id":125,"label":126,"row":54,"col":24,"span":34,"kind":26},"api","REST + SSE\ndashboard · \u002Fstatus",[128,129,130,131,132,133,134,135,137],{"n":25,"from":105,"to":111},{"n":34,"from":111,"to":108},{"n":45,"from":111,"to":35},{"n":54,"from":111,"to":114},{"n":56,"from":114,"to":122,"side":62},{"n":59,"from":122,"to":35},{"n":61,"from":35,"to":119},{"n":136,"from":119,"to":125},8,{"n":138,"from":114,"to":125,"side":62},9,[140,141,142,143,144,145,146,147,148],"The manager keeps one goroutine per check; the next run is scheduled after the previous one finishes, so checks of one address never overlap.","Run performs an HTTP GET (up to 5 redirects, expected status or any \u003C 400) or a TCP connect with a timeout.","The result is written to SQLite.","After the write, the result is published to the hub.","The detector gets the result: fail_threshold failures in a row open an incident, the first success closes it.","A state change is stored as an incident; an open incident survives a restart.","Uptime for 24 h, 7 and 30 days and p50\u002Fp95 are computed by queries over the raw history (pure functions in internal\u002Fstats).","REST serves the numbers to the dashboard and to the public status page (public checks only, without addresses or error texts).","The hub fans events out to SSE subscribers without blocking: a slow client loses events instead of slowing the checks down.",[150,153,156,159],{"title":151,"text":152},"Percentiles by the nearest-rank method","The p-th percentile is the element of the sorted slice at rank ceil(p\u002F100·N). A simple pure function without interpolation, easy to test.",{"title":154,"text":155},"Never overlap checks","The next run is scheduled after the previous one completes, so a slow target does not pile up parallel checks on itself.",{"title":157,"text":158},"A slow client does not slow monitoring","Hub delivery is non-blocking; after reconnecting, the dashboard re-reads the state.",{"title":160,"text":161},"SSE over fetch, not EventSource","This way the token travels in the Authorization header instead of the URL.",[163,164],"Uptime is computed from the number of checks, not durations, so figures for checks with different intervals are not equally precise.","Aggregates run over the raw history: instant for dozens of checks, thousands would need pre-aggregation. One node, one token, no notifications.","Scheduler, percentiles and uptime, incident lifecycle (including after a restart), HTTP\u002FTCP checks with a timeout, the API, SSE.","README of the uptime-monitor project (in Russian), sections \"Возможности\" (Features), \"Архитектура\" (Architecture), \"Ограничения\" (Limitations)",{"slug":99,"kind":90,"go_lines":168,"go_test_lines":169,"tests":170,"endpoints":171,"html":172,"css":173,"js":174},1445,589,18,15,126,172,369,{"slug":176,"title":177,"stack":178,"summary":181,"nodes":182,"edges":203,"flows":211,"decisions":219,"tradeoffs":232,"tests":235,"source":236,"stats":237},"realtime-rooms","Rooms: realtime chat and board",[13,179,14,180,18],"WebSocket","Canvas","WebSocket rooms: chat with history, presence, a shared board. The RFC 6455 protocol is written on the stdlib to show what happens under new WebSocket(…).",[183,186,189,191,194,197,200],{"id":184,"label":185,"row":24,"col":24,"span":34,"kind":26},"browsers","Browsers\nWebSocket clients",{"id":187,"label":188,"row":25,"col":24,"span":34,"kind":30},"ws","internal\u002Fws\nhandshake · frames · limits",{"id":114,"label":190,"row":34,"col":24,"span":34,"kind":30},"Room hub\nmutex per room",{"id":192,"label":193,"row":45,"col":24,"span":25,"kind":30},"queue","Client queue\n256 messages",{"id":195,"label":196,"row":45,"col":25,"span":25,"kind":30},"pump","write pump\n10 s write deadline",{"id":198,"label":199,"row":54,"col":24,"span":25,"kind":35},"sql","SQLite (WAL)\nchat history",{"id":201,"label":202,"row":54,"col":25,"span":25,"kind":35},"board","Board in memory\nstrokes",[204,205,206,207,208,209,210],{"n":25,"from":184,"to":187},{"n":34,"from":187,"to":114},{"n":45,"from":114,"to":192},{"n":54,"from":192,"to":195},{"n":56,"from":195,"to":184,"side":62},{"n":59,"from":114,"to":198,"side":57},{"n":61,"from":114,"to":201,"side":62},[212,213,214,215,216,217,218],"The browser opens a WebSocket: GET with Upgrade, version 13, a key and an Origin check; the answer is 101 with Sec-WebSocket-Accept.","Frames are parsed in internal\u002Fws: masking, fragmentation, size limit, UTF-8, ping\u002Fpong; the JSON message goes to the hub (read pump).","Broadcasting puts the message into each client's bounded queue; on overflow a slow client is disconnected with code 1008 and the others do not wait.","The write pump takes messages from the queue and writes frames with a 10 s deadline.","Frames reach the browsers; ephemeral messages (cursors, typing) are skipped when the queue is 3\u002F4 full.","A message is broadcast only after a successful SQLite write; on failure the sender gets error: store.","Board strokes are kept in room memory and given to participants who join later.",[220,223,226,229],{"title":221,"text":222},"Own WebSocket layer","About 530 lines in internal\u002Fws: handshake, frames, masking, fragmentation, ping\u002Fpong and close codes. The goal is to understand the protocol; production usually calls for a proven library.",{"title":224,"text":225},"Backpressure instead of waiting","A bounded queue per client and disconnecting the slow one: a single slow tab does not hold the room back.",{"title":227,"text":228},"Same order for everyone","Saving and broadcasting happen under one room mutex; the history in welcome is read under the same mutex, so there is neither overlap nor a gap with the live stream.",{"title":230,"text":231},"Per-connection limits","A token bucket for chat (2 messages\u002Fs, burst 5) and an overall limit (200\u002Fs, burst 400); flooding closes the connection with code 1008.",[233,234],"A single process: the hub lives in memory, with no horizontal scaling. The board does not survive a server restart and any participant can clear it.","No permessage-deflate, TLS or authentication; the Autobahn Test Suite was not run. Nicknames are anonymous and unprotected.","The handshake example from the RFC, 12 kinds of protocol violations with close-code checks, fragmentation with a ping in the middle, slow-client disconnects, and a load test: 360 connections in 6 rooms.","README of the realtime-rooms project (in Russian), sections \"Почему свой слой WebSocket\" (Why a custom WebSocket layer), \"Возможности\" (Features), \"Гарантии\" (Guarantees), \"Ограничения\" (Limitations)",{"slug":176,"kind":90,"go_lines":238,"go_test_lines":239,"tests":240,"endpoints":61,"html":241,"css":242,"js":243},1822,1313,48,103,184,517,{"slug":245,"title":246,"stack":247,"summary":248,"nodes":249,"edges":265,"flows":272,"decisions":279,"tradeoffs":286,"tests":289,"source":290,"stats":291},"booking-slots","Appointment booking",[13,14,18],"Online booking: free slots from service duration and working hours, double-booking protection, an admin panel.",[250,252,255,257,260,263],{"id":26,"label":251,"row":24,"col":24,"span":25,"kind":26},"Client\nservice · day · slot",{"id":253,"label":254,"row":24,"col":25,"span":25,"kind":26},"admin","Admin \u002Fadmin\nBearer token",{"id":125,"label":256,"row":25,"col":24,"span":34,"kind":30},"HTTP API\nslots · bookings · admin",{"id":258,"label":259,"row":34,"col":24,"span":25,"kind":30},"slots","Slot calculation\nhours minus taken",{"id":261,"label":262,"row":34,"col":25,"span":25,"kind":30},"book","Booking\none transaction",{"id":32,"label":264,"row":45,"col":24,"span":34,"kind":35},"SQLite\nUNIQUE (date, start_min) WHERE active",[266,267,268,269,270,271],{"n":25,"from":26,"to":125},{"n":34,"from":253,"to":125},{"n":45,"from":125,"to":258},{"n":54,"from":125,"to":261},{"n":56,"from":258,"to":32},{"n":59,"from":261,"to":32},[273,274,275,276,277,278],"The client requests 14 days ahead and the slots of a day, then sends POST \u002Fapi\u002Fbookings.","The owner manages bookings, services, working hours and closed dates.","Slots are computed from the service duration, the weekday hours and the taken intervals, on a 30-minute grid.","Book checks that the slot is free and inserts the booking in one transaction.","The calculation reads working hours, closed dates and active bookings.","The insert is guarded by a partial UNIQUE index; on conflict the client gets 409.",[280,283],{"title":281,"text":282},"Two lines of defence against double booking","The availability check and the insert run in one transaction, and a partial UNIQUE index on (date, start_min) for active bookings catches whatever the check missed. A cancelled booking frees the time.",{"title":284,"text":285},"Time zone as configuration","The zone comes from TZ_NAME and the tzdata database is embedded in the binary, so the schedule does not depend on the machine's settings.",[287,288],"One specialist\u002Fresource: all services share one schedule.","Closing a date does not cancel existing bookings; the client cannot cancel on their own, there are no notifications; the admin panel uses a shared token.","Slot calculation and period bounds, overlaps, concurrent booking: 30 goroutines for one time slot, exactly one succeeds.","README of the booking-slots project (in Russian), sections \"Возможности\" (Features), \"Тесты\" (Tests), \"Ограничения\" (Limitations), and the code in internal\u002Fbooking",{"slug":245,"kind":90,"go_lines":292,"go_test_lines":293,"tests":294,"endpoints":170,"html":295,"css":296,"js":297},891,338,12,146,123,309,{"slug":299,"title":300,"stack":301,"summary":302,"nodes":303,"edges":319,"flows":326,"decisions":333,"tradeoffs":337,"tests":340,"source":341,"stats":342},"link-shortener","Short: link shortener",[13,14,18],"A link shortener with click statistics: aliases, expiry, 302 redirects, a token-protected dashboard.",[304,307,309,312,315,317],{"id":305,"label":306,"row":24,"col":24,"span":25,"kind":26},"visitor","Visitor\nGET \u002F{code}",{"id":253,"label":308,"row":24,"col":25,"span":25,"kind":26},"Admin\ndashboard · token",{"id":310,"label":311,"row":25,"col":24,"span":34,"kind":30},"http","net\u002Fhttp\nlink creation rate limit",{"id":313,"label":314,"row":34,"col":24,"span":25,"kind":30},"redirect","302 redirect\nand click tracking",{"id":125,"label":316,"row":34,"col":25,"span":25,"kind":30},"Links API\nCRUD · statistics",{"id":32,"label":318,"row":45,"col":24,"span":34,"kind":35},"SQLite\nlinks · clicks",[320,321,322,323,324,325],{"n":25,"from":305,"to":310},{"n":34,"from":253,"to":310},{"n":45,"from":310,"to":313},{"n":54,"from":310,"to":125},{"n":56,"from":313,"to":32},{"n":59,"from":125,"to":32},[327,328,329,330,331,332],"A visitor opens a short link.","The admin creates links and views statistics with a Bearer token.","The public route answers with a 302 redirect.","Link creation is rate limited per address.","A click is stored with a visitor hash (daily salt): by day, browser, OS and device; IP addresses are not stored.","Statistics are built from the stored clicks.",[334],{"title":335,"text":336},"Privacy-friendly statistics","Unique visitors are counted by a hash with a daily salt; IP addresses are not stored.",[338,339],"A learning example: one admin token, no users.","Demo statistics are generated and do not reflect real traffic.","go vet and go test in CI; the project README does not list the coverage in detail.","README of the link-shortener project (in Russian), sections \"Возможности\" (Features), \"Ограничения\" (Limitations)",{"slug":299,"kind":90,"go_lines":292,"go_test_lines":343,"tests":294,"endpoints":344,"html":345,"css":346,"js":347},328,11,119,162,317,{"slug":349,"title":350,"stack":351,"summary":352,"nodes":353,"edges":367,"flows":373,"decisions":379,"tradeoffs":383,"tests":386,"source":387,"stats":388},"kanban-tasks","Amber: kanban board",[13,14,18],"A multi-user kanban board: registration, boards, columns, drag-and-drop cards.",[354,357,359,362,365],{"id":355,"label":356,"row":24,"col":24,"span":34,"kind":26},"browser","Browser\nvanilla JS · drag and drop",{"id":310,"label":358,"row":25,"col":24,"span":34,"kind":30},"HTTP API (JSON)\nContent-Type · same-origin",{"id":360,"label":361,"row":34,"col":24,"span":25,"kind":30},"auth","Authentication\nPBKDF2 · sessions",{"id":363,"label":364,"row":34,"col":25,"span":25,"kind":30},"boards","Boards and cards\nper-user data",{"id":32,"label":366,"row":45,"col":24,"span":34,"kind":35},"SQLite\nno CGO",[368,369,370,371,372],{"n":25,"from":355,"to":310},{"n":34,"from":310,"to":360},{"n":45,"from":310,"to":363},{"n":54,"from":360,"to":32},{"n":56,"from":363,"to":32},[374,375,376,377,378],"The browser talks to the JSON API; mutating requests are checked for Content-Type and same-origin.","Sign-up and login by email and password: PBKDF2-HMAC-SHA256, a session in an HttpOnly cookie with SameSite=Lax.","Boards, columns and cards are available only to their owner: user data is isolated.","Users and sessions are stored in SQLite.","Boards, columns, cards and their order are stored in SQLite.",[380],{"title":381,"text":382},"Protection without external libraries","PBKDF2 from the standard library, an HttpOnly session, and Content-Type plus same-origin checks for mutating requests.",[384,385],"A learning example: no password reset, email verification or login rate limiting.","Do not expose it to the internet without HTTPS and a reverse proxy.","API tests (go vet and go test in CI).","README of the kanban-tasks project (in Russian), sections \"Возможности\" (Features), \"Ограничения\" (Limitations)",{"slug":349,"kind":90,"go_lines":389,"go_test_lines":390,"tests":61,"endpoints":170,"html":391,"css":392,"js":343},871,272,102,156,{"slug":394,"title":395,"stack":396,"summary":405,"nodes":406,"edges":434,"flows":444,"decisions":454,"tradeoffs":467,"tests":470,"source":471,"stats":472},"api-gateway","Sluice: API gateway",[13,397,398,399,400,16,17,401,402,403,18,404],"stdlib only","Reverse proxy","Circuit breaker","Token bucket","slog","Vanilla JS","embed","GitHub Actions","An API gateway on the pure stdlib: routes from a JSON config with hot reload, balancing, health checks, a per-node circuit breaker, retries, rate limiting. The point is immutable state and protection against open-proxy and SSRF.",[407,409,412,415,418,421,424,427,430,431],{"id":26,"label":408,"row":24,"col":24,"span":25,"kind":26},"Client\nHTTP on :8087",{"id":410,"label":411,"row":24,"col":25,"span":25,"kind":26},"ops","Operator\nSIGHUP · PUT \u002Fapi\u002Fconfig",{"id":413,"label":414,"row":25,"col":24,"span":34,"kind":30},"entry","Gateway entry\nrequest-id · host + prefix",{"id":416,"label":417,"row":34,"col":24,"span":25,"kind":30},"guard","Auth and rate limit\nkey sha256 · token bucket",{"id":419,"label":420,"row":34,"col":25,"span":25,"kind":35},"state","State\natomic.Pointer",{"id":422,"label":423,"row":45,"col":24,"span":25,"kind":30},"bal","Balancer\nround-robin · least-conn",{"id":425,"label":426,"row":45,"col":25,"span":25,"kind":30},"health","Health checks\nGET \u002Fhealth per node",{"id":428,"label":429,"row":54,"col":24,"span":25,"kind":30},"brk","Circuit breaker\nclosed · open · half-open",{"id":47,"label":48,"row":54,"col":25,"span":25,"kind":26},{"id":432,"label":433,"row":56,"col":24,"span":34,"kind":30},"up","Upstreams\nSSRF guard · retries",[435,436,437,438,439,440,441,442,443],{"n":25,"from":26,"to":413},{"n":34,"from":410,"to":419,"side":62},{"n":45,"from":419,"to":413},{"n":54,"from":413,"to":416},{"n":56,"from":416,"to":422},{"n":59,"from":425,"to":422},{"n":61,"from":422,"to":428},{"n":136,"from":428,"to":432},{"n":138,"from":432,"to":47,"side":62},[445,446,447,448,449,450,451,452,453],"A request arrives on :8087, gets a request-id and the route is looked up by host and longest prefix; no match gives 404, the gateway is not an open proxy.","The operator changes the config via SIGHUP (Unix only) or PUT\u002FPOST in the admin on :8088; the config is validated as a whole, a bad one is rejected and the old one keeps running.","The new state (routes, upstreams, keys) is swapped with a single atomic.Pointer: the listening socket is untouched and an in-flight request finishes on the old version.","The API key is hashed with sha256 and checked against the config; the token-bucket rate limit is kept per IP (IPv6 by \u002F64) or per key, and an excess request never reaches the upstream.","The balancer picks a node by round-robin or least-conn, among healthy nodes only.","A separate health loop per node polls GET \u002Fhealth and marks the node healthy or not by thresholds.","Each node has its own breaker (closed, open, half-open); a connection failure, a timeout and any 5xx except 501 count as errors, a client aborting the request does not.","The connection goes through a guarded Dialer that checks the IP at connect time (SSRF, DNS rebinding); on 502\u002F503\u002F504 or a network failure GET, HEAD, OPTIONS, PUT and DELETE are retried on another node.","The statistics window and counters go to the dashboard over SSE and to \u002Fmetrics in Prometheus format; labels are bounded by the config, paths and keys never become labels.",[455,458,461,464],{"title":456,"text":457},"Immutable state behind an atomic.Pointer","Routes, upstreams and keys are built into a new state and swapped with one pointer. Upstreams whose description did not change are reused as is, so health, breaker and counters are not reset on reload.",{"title":459,"text":460},"Not an open proxy, SSRF protection","The destination comes only from the config; Host, absolute-URI, CONNECT\u002FTRACE and paths with \"..\" cannot escape the routes. The IP a name resolves to is checked again at connect time, and allowing private networks is set by the environment, not the config.",{"title":462,"text":463},"A breaker per node","The closed, open and half-open states are kept per node: one sick node drops out of rotation while the others keep taking traffic.",{"title":465,"text":466},"Retries for idempotent requests only","POST is not repeated so an operation is never run twice; the body is buffered up to 1 MiB and larger requests go without retries.",[468,469],"Rate limit and breaker state live in the memory of one process: with several replicas the limits are not shared, and behind a load balancer all clients look like one IP because the gateway does not trust X-Forwarded-For.","No TLS on the inbound side, WebSocket and Upgrade are not supported (501); percentiles come from a histogram with up to 25% error; the admin has a single token; SIGHUP is Unix only.","Balancers, breaker states on a fake clock, token bucket, SSRF cases, retries (GET, PUT with a body; POST is not retried), hot reload with an in-flight request, open-proxy protection, a load test (4000 requests, 40 clients); go test -race in CI.","README of the api-gateway project (in Russian), sections \"Архитектура\" (Architecture), \"Гарантии\" (Guarantees), \"Что считается ошибкой\" (What counts as an error), \"Ограничения\" (Limitations)",{"slug":394,"kind":90,"go_lines":473,"go_test_lines":474,"tests":240,"endpoints":344,"html":475,"css":476,"js":477},2334,1179,115,139,163,{"slug":479,"title":480,"stack":481,"summary":487,"nodes":488,"edges":511,"flows":520,"decisions":529,"tradeoffs":542,"tests":545,"source":546,"stats":547},"notes-search","Card Index: notes with search",[13,14,482,483,484,15,485,486,402,403,18,404],"FTS5","bm25","modernc.org\u002Fsqlite (no CGO)","Markdown (custom safe renderer)","SVG force-layout","A Markdown notes base on Go and SQLite: wiki links with backlinks and a graph, FTS5 search with bm25, versions with an own diff. The point is a safe own renderer and search over stems.",[489,492,495,497,500,503,505,508],{"id":490,"label":491,"row":24,"col":24,"span":25,"kind":26},"editor","Editor\nautosave · rev",{"id":493,"label":494,"row":24,"col":25,"span":25,"kind":26},"ui","Search and graph\nCtrl+K · SVG layout",{"id":125,"label":496,"row":25,"col":24,"span":34,"kind":30},"REST API \u002Fapi\nBearer token",{"id":498,"label":499,"row":34,"col":24,"span":25,"kind":30},"md","Markdown renderer\nallow-list · [[links]]",{"id":501,"label":502,"row":34,"col":25,"span":25,"kind":30},"text","Stemmer and query\nstems · prefixes",{"id":32,"label":504,"row":45,"col":24,"span":34,"kind":35},"SQLite\nnotes · links · versions · notes_fts",{"id":506,"label":507,"row":54,"col":24,"span":25,"kind":30},"ver","Versions and diff\nLCS · merge window",{"id":509,"label":510,"row":54,"col":25,"span":25,"kind":26},"io","Export\nzip of .md",[512,513,514,515,516,517,518,519],{"n":25,"from":490,"to":125},{"n":34,"from":493,"to":125},{"n":45,"from":125,"to":498},{"n":54,"from":125,"to":501},{"n":56,"from":498,"to":32},{"n":59,"from":501,"to":32},{"n":61,"from":32,"to":506},{"n":136,"from":32,"to":509},[521,522,523,524,525,526,527,528],"The editor sends PUT with rev; the list on the left updates immediately while typing and rolls back if the server returns an error.","Search, Ctrl+K and the graph call \u002Fapi\u002Fsearch and \u002Fapi\u002Fgraph with a Bearer token; the graph is laid out in the browser (Fruchterman-Reingold, SVG).","The note text goes to the own renderer: the source is escaped as a whole, only the renderer creates tags from a closed set, link schemes come from an allow-list.","The title, body and tags go through tokenization and a stemmer (about 70 endings, ё → е); the query becomes \"stem\"* terms joined with AND, and user input never reaches FTS syntax.","[[Links]] are extracted from the text and written to links by title_key; backlinks are a join on dst_key, so a link to a note that does not exist yet comes alive on its own.","Stems go into notes_fts, ranking is bm25(notes_fts, 8, 1, 4) (title, body, tags), Go code builds the snippet; saving is one transaction: note, tags, links, FTS and version.","Autosave edits within 5 minutes are merged into the last version, Ctrl+S creates a new one; the diff is line-based on LCS; on a stale rev the server answers 409 with the current note.","Export returns a zip of .md files with front matter; importing a folder, files or a zip goes through the API, and archive paths are never written anywhere, so zip-slip is impossible.",[530,533,536,539],{"title":531,"text":532},"An own Markdown renderer instead of goldmark","Safety is a property of the design, not cleanup afterwards: the source is escaped and tags come from a closed set. The price is fewer features than CommonMark (no tables or images); in return the dependencies stay at stdlib and SQLite.",{"title":534,"text":535},"FTS5 over stems, snippet in Go","The index holds stems, not the original text, so the built-in FTS5 snippet() would only see stems. Go code highlights the matches and escapes the HTML.",{"title":537,"text":538},"title_key instead of NOCASE","Uniqueness and wiki links are built on the lower-cased title with normalized whitespace, because SQLite NOCASE does not work for Cyrillic.",{"title":540,"text":541},"rev for conflicts, ver for history","rev counts any change and guards against overwriting another edit, ver numbers the versions. \"Overwrite with mine\" snapshots a version, so the other edit stays in the history.",[543,544],"Morphology is simplified: fleeting vowels, alternations and irregular forms do not work, and short words can produce false matches. Renaming a note does not rewrite [[links]] in other notes.","One shared token, one database connection, conflicts are resolved wholesale; the graph is meant for hundreds of notes (O(n²) layout per step). Markdown has no tables, images or footnotes.","bm25 ranking and title weight, morphology, junk queries, backlinks and graph, versions and edit merging, rev conflicts, XSS (about 30 payloads), diff, export → import, zip safety; go test -race in CI.","README of the notes-search project (in Russian), sections \"Архитектура\" (Architecture), \"Схема БД\" (Database schema), \"Поиск\" (Search), \"Решения\" (Decisions), \"Ограничения\" (Limitations)",{"slug":479,"kind":90,"go_lines":548,"go_test_lines":389,"tests":549,"endpoints":94,"html":550,"css":551,"js":243},2027,32,174,192,{"slug":553,"title":554,"stack":555,"summary":564,"nodes":565,"edges":586,"flows":595,"decisions":604,"tradeoffs":617,"tests":620,"source":621,"stats":622},"raft-kv","Raft KV: a key-value store on Raft",[13,556,557,558,397,559,16,560,561,562,563,402,18,404],"Raft","Distributed systems","Consensus","HTTP\u002FJSON","WAL + fsync","Snapshots","Linearizability","Deterministic simulation testing","A teaching implementation of Raft on the pure stdlib: three nodes, elections, replication, quorum commit, a WAL with fsync, snapshots, read-index. The point is a core with no time and no network, which the same code runs both in the runtime and in a deterministic simulator.",[566,568,570,573,575,578,580,583],{"id":26,"label":567,"row":24,"col":24,"span":25,"kind":26},"Client\nPUT · GET · DELETE",{"id":47,"label":569,"row":24,"col":25,"span":25,"kind":26},"Dashboard :8098\nSSE · failures (demo)",{"id":571,"label":572,"row":25,"col":24,"span":34,"kind":30},"node","Node runtime\nHTTP\u002FJSON · 25 ms tick · 307",{"id":30,"label":574,"row":34,"col":24,"span":25,"kind":30},"raft.Node\nTick() · Step(msg)",{"id":576,"label":577,"row":34,"col":25,"span":25,"kind":26},"peers","The two other nodes\nvote · append · snapshot",{"id":35,"label":579,"row":45,"col":24,"span":25,"kind":35},"Storage\nWAL + fsync · state.json",{"id":581,"label":582,"row":45,"col":25,"span":25,"kind":30},"kv","KV state machine\nput · del · snapshot",{"id":584,"label":585,"row":54,"col":24,"span":34,"kind":30},"sim","Simulator\nvirtual time · seed",[587,588,589,590,591,592,593,594],{"n":25,"from":26,"to":571},{"n":34,"from":47,"to":571},{"n":45,"from":571,"to":30},{"n":54,"from":30,"to":576},{"n":56,"from":30,"to":581},{"n":59,"from":30,"to":35,"side":57},{"n":61,"from":581,"to":35},{"n":136,"from":584,"to":35},[596,597,598,599,600,601,602,603],"A client sends PUT, GET or DELETE to any of the nodes 8095–8097; a non-leader answers 307 with a Location pointing at the leader (and an X-Raft-Leader header), and if the leader is unknown, 503 with Retry-After.","Every 200 ms the dashboard gets role, term, commit\u002Fapplied and the log tail over SSE; the \"stop node\", \"cut the network\" and \"kill the leader\" buttons exist only with SEED_DEMO=1 and go over real HTTP.","The runtime ticks the core every 25 ms (Tick) and hands it incoming messages (Step); outgoing messages are returned to the caller, raft.Node itself knows nothing about time or the network.","Elections (vote), replication (append) and snapshot travel as HTTP\u002FJSON messages, and replies are separate messages, so the network may drop and reorder packets; an index commits when a majority stores it and the entry belongs to the current term.","Committed entries are applied to the state machine (a dictionary with put and del commands); a PUT is answered after the quorum commit, and a linearizable GET waits for applied >= readIndex.","A reply goes out only after fsync: term and votedFor are written to state.json (temp file, fsync, rename) and log entries to a WAL with CRC32; a torn tail after a crash is truncated on load.","After SNAPSHOT_EVERY applied entries (200 by default) the snapshot of the dictionary is written as a new file generation: a new WAL, then an atomic publish of snapshot.bin, then the old WAL is deleted.","The same raft.Node runs in the simulator: time is virtual, the network is controlled (delays, loss, partitions), and a node crash and restart come from its storage; one seed reproduces a run byte for byte, and invariants and linearizability are checked after every step.",[605,608,611,614],{"title":606,"text":607},"A core with no goroutines or clocks","raft.Node receives time only through Tick() and messages through Step(), and returns outgoing messages to the caller. So the same code runs in the runtime (25 ms tick, HTTP) and in the simulator, and a failure is reproduced from a seed.",{"title":609,"text":610},"Commit only entries of the current term","An index commits when a majority stores it and the entry belongs to the current term (section 5.4.2 of the paper), so a new leader appends a no-op at once. There is no dedicated test for figure 8, and that is listed in the limitations.",{"title":612,"text":613},"Read-index instead of leases","The leader notes commitIndex, sends a heartbeat, waits for a majority to confirm and only then answers a GET. Correctness does not depend on clocks, and a partitioned leader cannot answer.",{"title":615,"text":616},"Snapshots by generations","A new WAL, then an atomic publish of snapshot.bin (the commit point), then deletion of the old WAL. A crash at any step leaves the whole old or the whole new state.",[618,619],"The membership is fixed (exactly three nodes); there is no dynamic membership, pre-vote or check-quorum, no client sessions or exactly-once: after a 504 the outcome is unknown. A snapshot goes as one message and sits in memory whole, every PUT is a separate fsync, and there is no TLS or authentication.","The verification is home-made: a simulator with a perfect disk and a linearizability check on small histories, which is not Jepsen. One hand-made mutation, committing entries of earlier terms, is not caught by the tests.","One leader per term (40 seeds with loss, crashes and partitions), re-election after the leader dies, replication, no commit without a quorum, rollback of a partitioned leader's tail, restart from disk, no double vote after a restart, snapshots and catch-up, linearizability under chaos (30 seeds), trace determinism; node HTTP tests and the linearizability checker itself; go test -race in CI.","README of the raft-kv project (in Russian), sections \"Архитектура\" (Architecture), \"Как устроены части\" (How the parts work), \"Инварианты\" (Invariants), \"Тесты\" (Tests), \"Чего здесь нет\" (What is missing)",{"slug":553,"kind":90,"go_lines":623,"go_test_lines":624,"tests":625,"endpoints":626,"html":627,"css":550,"js":628},2646,895,27,13,91,213,{"slug":630,"title":631,"stack":632,"summary":641,"nodes":642,"edges":663,"flows":671,"decisions":679,"tradeoffs":692,"tests":695,"source":696,"stats":697},"auth-service","Keyring: an authentication service",[13,633,14,484,634,635,636,637,638,639,640,402,403,18,404],"stdlib crypto","JWT EdDSA\u002FEd25519","JWKS","PBKDF2","TOTP","RBAC","REST","OpenAPI 3","An authentication service on stdlib crypto and SQLite: EdDSA JWTs with key rotation and JWKS, refresh tokens with rotation and reuse detection, TOTP, RBAC, lockout, audit. The point is the token flows and an honest threat model with what stays unaddressed.",[643,645,648,650,652,655,657,660],{"id":355,"label":644,"row":24,"col":24,"span":25,"kind":26},"Browsers and clients\nBearer · rt cookie",{"id":646,"label":647,"row":24,"col":25,"span":25,"kind":26},"consumer","Third-party service\nverifies via JWKS",{"id":310,"label":649,"row":25,"col":24,"span":34,"kind":30},"HTTP layer\nCSP · Origin · CSRF · IP limit",{"id":30,"label":651,"row":34,"col":24,"span":25,"kind":30},"Auth core\nPBKDF2 · TOTP · RBAC",{"id":653,"label":654,"row":34,"col":25,"span":25,"kind":30},"jwt","JWT EdDSA\nkid · key ring",{"id":32,"label":656,"row":45,"col":24,"span":25,"kind":35},"SQLite\nsessions · tokens · keys",{"id":658,"label":659,"row":45,"col":25,"span":25,"kind":30},"jwks","JWKS\n\u002F.well-known\u002Fjwks.json",{"id":661,"label":662,"row":54,"col":24,"span":34,"kind":35},"audit","Audit log\nappend-only",[664,665,666,667,668,669,670],{"n":25,"from":355,"to":310},{"n":34,"from":310,"to":30},{"n":45,"from":30,"to":653},{"n":54,"from":30,"to":32},{"n":56,"from":653,"to":658},{"n":59,"from":658,"to":646,"side":62},{"n":61,"from":32,"to":661},[672,673,674,675,676,677,678],"Sign-in: login and password, with a second step of a TOTP or backup code when 2FA is on; the response carries a 10-minute access JWT, and the refresh token arrives in an HttpOnly SameSite=Strict cookie together with a CSRF token.","Middleware checks Origin, CSRF and the per-IP rate, then the access token signature and expiry, then in the database that the session is alive; permissions come from the database on every request, so revocation and role changes take effect at once.","Tokens are signed by a ring of Ed25519 keys: only EdDSA is accepted and the kid comes from an allow-list; POST \u002Fapi\u002Fadmin\u002Fkeys\u002Frotate issues a new key, and the old one stays for verification for another access_ttl + 1 minute.","The database holds hashes of the password (PBKDF2, 600,000 iterations), refresh tokens (SHA-256) and backup codes; a refresh burns the old token and issues a new one, while presenting a used one revokes the whole family and logs token.reuse_detected.","The public keys are served at \u002F.well-known\u002Fjwks.json (RFC 7517\u002F8037) along with their kid.","A third-party service takes the kid from the JWT header, fetches the key from JWKS (cached for a minute) and verifies the signature, iss, aud, typ and exp; it learns of a session revocation no earlier than the end of the access token's life.","Sign-ins, failures, lockouts, 2FA, password resets, session revocations, admin actions, key rotation and permission denials are written to the audit table; database triggers forbid UPDATE and DELETE, and passwords and tokens never reach the log.",[680,683,686,689],{"title":681,"text":682},"A database check on every request","The access token is checked against the database (the session is not revoked, the user is not disabled, permissions are current). It costs one SQLite query and in return gives immediate \"sign out everywhere\", blocking and role changes.",{"title":684,"text":685},"A refresh token family and reuse detection","Every refresh burns the old token and issues a new one. If a used token shows up again it is assumed stolen: the whole session is revoked and both the stolen and the legitimate token are dead.",{"title":687,"text":688},"EdDSA only, and a JWT parser written on the stdlib","The none and HS256 algorithms are rejected before the signature check (alg-confusion defence), the kid comes from an allow-list and the token length is bounded. The tests include HS256 with the public key used as the secret.",{"title":690,"text":691},"The same response and timing for an unknown login","For a nonexistent user a dummy hash is computed, and the response and timing match a wrong password, while forgot always answers 202. One leak remains: registration says the login or e-mail is taken.",[693,694],"The TOTP secret and the private signing keys sit in the database in plain text: with a dump you can forge tokens and 2FA. Passwords use PBKDF2 rather than argon2id, and backup codes a fast SHA-256.","It has not been independently audited and has no OAuth\u002FOIDC, WebAuthn or e-mail confirmation; the reset e-mail is a stub in the log. The limiter lives in memory and everything in the SQLite of a single process, and an access token cannot be revoked at third-party consumers before exp.","JWT (forgery, exp\u002Fnbf\u002Fiss\u002Faud\u002Ftyp, alg=none, HS256 with the public key, rotation), TOTP against the RFC 6238 and RFC 4226 vectors, refresh rotation and reuse detection, lockout with exponential delay, 2FA and backup codes, RBAC, password reset, audit (UPDATE\u002FDELETE forbidden), cookie flags, CSRF, CORS, Origin, rate limit; JWKS is checked by independent code; go test -race in CI.","README of the auth-service project (in Russian), sections \"Возможности\" (Features), \"Потоки токенов\" (Token flows), \"Модель угроз\" (Threat model), \"Схема БД\" (Database schema), \"Ограничения\" (Limitations)",{"slug":630,"kind":90,"go_lines":698,"go_test_lines":699,"tests":700,"endpoints":701,"html":702,"css":703,"js":704},2826,1393,50,35,23,166,500,{"slug":706,"title":707,"stack":708,"summary":717,"nodes":718,"edges":741,"flows":753,"decisions":764,"tradeoffs":777,"tests":780,"source":781,"stats":782},"finance-tracker","Ledger: personal finance tracker",[13,14,484,15,709,710,711,712,713,714,715,18,404,636,716],"Nuxt 4","Vue 3","TypeScript","Pinia","SVG charts","vitest","ESLint","CSV import\u002Fexport","Personal finance tracking: a Go binary with SQLite and an embedded Nuxt 4 front end (an SPA). The point is money in integer minor units, multi-currency with manual rates, a schedule and CSV import, and user isolation.",[719,722,725,727,730,733,736,739],{"id":720,"label":721,"row":24,"col":24,"span":25,"kind":26},"spa","Nuxt 4 SPA\nPinia · SVG charts",{"id":723,"label":724,"row":24,"col":25,"span":25,"kind":26},"csv","Bank CSV\nimport · export",{"id":125,"label":726,"row":25,"col":24,"span":34,"kind":30},"Go HTTP API\nsession cookie · Origin · user_id",{"id":728,"label":729,"row":34,"col":24,"span":25,"kind":30},"imp","CSV import\npreview · dry run",{"id":731,"label":732,"row":34,"col":25,"span":25,"kind":30},"rec","Schedule\ncron rule · catch-up",{"id":734,"label":735,"row":45,"col":24,"span":25,"kind":30},"ledger","Accounts and transactions\nminor units · big.Int",{"id":737,"label":738,"row":45,"col":25,"span":25,"kind":30},"rep","Budgets and reports\nmonth · trends",{"id":32,"label":740,"row":54,"col":24,"span":34,"kind":35},"SQLite\nmodernc · no CGO",[742,743,744,745,746,747,748,749,750,751],{"n":25,"from":720,"to":125},{"n":34,"from":723,"to":125},{"n":45,"from":125,"to":734,"side":57},{"n":54,"from":125,"to":728},{"n":56,"from":125,"to":731},{"n":59,"from":728,"to":734},{"n":61,"from":731,"to":734},{"n":136,"from":734,"to":737},{"n":138,"from":734,"to":32},{"n":752,"from":737,"to":32},10,[754,755,756,757,758,759,760,761,762,763],"The browser talks to the API in JSON; the session lives in an httpOnly cookie (SameSite=Lax), the database stores only the SHA-256 of the token, and Origin is checked on write requests.","A CSV file and exports (CSV protected against formula injection, and a full JSON) go through the same API, with no separate channels.","Every request is bound to the user_id from the session; ordinary CRUD of accounts, categories, rates and transactions goes straight to the accounts and transactions layer, with filters q, kind, account_id, category_id, tag, from and to.","POST \u002Fapi\u002Fimport\u002Fpreview detects the delimiter and encoding and shows the column mapping; \u002Fcommit writes the rows, skips duplicates and returns a report of bad rows (at most 20,000 rows at a time).","A cron-like rule of five fields (day precision) is executed when the app is opened and by the \u002Fapi\u002Frecurring\u002Frun button; missed dates are caught up, and there is no background scheduler.","Imported rows become ordinary transactions: duplicates are skipped, missing categories are created, and transfers are not imported.","Recurring payments also become ordinary transactions and keep a recurring_id; the rule’s last_date moves forward.","Reports read the transactions whole into memory and compute in the base currency at current rates; the trend slope is a least-squares fit in integers.","Amounts are written as integer minor units (amount_minor > 0), a rate is stored as an integer x10^6, conversion goes through big.Int; an account balance is not stored but computed by a query.","Budget limits (limit_minor) and warning thresholds (warn_pct) live in the budgets table and are compared with the month’s spending; reports read the same tables and write nothing.",[765,768,771,774],{"title":766,"text":767},"Money only in integer minor units","In the database, API and browser amounts are integers of cents, with no floats. Currency conversion goes through big.Int with round-half-away-from-zero, and rounding and amount parsing are covered by tests.",{"title":769,"text":770},"Hand-made SVG charts instead of a library","Only three simple chart types are needed, and a library would add tens of KB of JS. The own ones (about 250 lines) give control over accessibility (role=\"img\", a text table), theming through CSS variables and a strict CSP.",{"title":772,"text":773},"An SPA without SSR","Nuxt is built with nuxt generate and ssr:false because session data cannot be prerendered. Go serves the embedded web\u002Fdist and 200.html for unknown paths, and the CSP gets hashes of the inline scripts instead of unsafe-inline for scripts.",{"title":775,"text":776},"Balance is computed, not stored","An account balance = opening balance + income - expenses - outgoing transfers + incoming transfers. There is no stored field, so it cannot drift from the transactions.",[778,779],"Rates are manual and current, with no rate history: the past is recomputed at today's rates. The base currency cannot be changed, a budget is the same every month, and payments are created only when the app is opened.","No password change or recovery, SQLite and a single process, and reports read transactions whole into memory. style-src still allows unsafe-inline (Vue styles), and SSR is off.","Money (parsing, formatting, rounding, conversion, round-trip), budgets, cron rules and catch-up of missed dates, CSV import, user isolation, the API, reports, demo data, TestRealDist (the embedded build is served, the CSP has no unsafe-inline for scripts); on the front end vitest, ESLint and typecheck; go test -race in CI.","README of the finance-tracker project (in Russian), sections \"Возможности\" (Features), \"Архитектура\" (Architecture), \"Схема БД\" (Database schema), \"API\", \"Ограничения\" (Limitations)",{"slug":706,"kind":90,"go_lines":783,"go_test_lines":784,"tests":785,"endpoints":786,"html":787,"css":788,"js":789},3741,1371,30,45,24,1148,72,{"slug":791,"title":792,"stack":793,"summary":797,"nodes":798,"edges":820,"flows":829,"decisions":838,"tradeoffs":851,"tests":854,"source":855,"stats":856},"pixel-wall","Pixel Wall: a shared canvas",[13,397,179,794,402,403,795,796,18,404],"Canvas 2D","Binary protocol","Snapshot + journal","A shared 256x256 canvas over WebSocket: an own RFC 6455 on the stdlib, a binary protocol, a snapshot and deltas, a snapshot plus a journal. The point is delta consistency and crash recovery, with every pixel costing 4 bits.",[799,801,803,806,809,812,815,818],{"id":355,"label":800,"row":24,"col":24,"span":25,"kind":26},"Browser\nCanvas 2D · zoom · timelapse",{"id":253,"label":802,"row":24,"col":25,"span":25,"kind":26},"Admin\nBearer ADMIN_TOKEN",{"id":804,"label":805,"row":25,"col":24,"span":34,"kind":30},"srv","HTTP + WebSocket\nOrigin · limits · 50 ms hub",{"id":807,"label":808,"row":34,"col":24,"span":25,"kind":30},"wall","wall core\none mutex · seq",{"id":810,"label":811,"row":34,"col":25,"span":25,"kind":30},"cool","Cooldown and bans\nsession · IP · rate limit",{"id":813,"label":814,"row":45,"col":24,"span":25,"kind":30},"canvas","Canvas\n4 bits per pixel",{"id":816,"label":817,"row":45,"col":25,"span":25,"kind":30},"hist","History in memory\nold and new color",{"id":35,"label":819,"row":54,"col":24,"span":34,"kind":35},"Disk\ncanvas.snap + journal.log",[821,822,823,824,825,826,827,828],{"n":25,"from":355,"to":804},{"n":34,"from":253,"to":804},{"n":45,"from":804,"to":807},{"n":54,"from":807,"to":810},{"n":56,"from":807,"to":813},{"n":59,"from":807,"to":816},{"n":61,"from":813,"to":35},{"n":136,"from":816,"to":35},[830,831,832,833,834,835,836,837],"The browser opens WS \u002Fws?sid=..., receives a compressed snapshot (0x01: seq, size, the deflate-raw canvas) and then only deltas (0x02: firstSeq and up to 2000 records); a pixel goes out in 6 bytes (0x10: x, y, color).","Admin requests carry Authorization: Bearer; the token is compared in constant time, wrong attempts are rate-limited per IP, and without ADMIN_TOKEN the admin API is off (403).","The server checks Origin, message sizes (WS up to 64 bytes, body up to 4 KiB) and the signed session, then passes the placement to wall; the hub broadcasts accumulated deltas every 50 ms, and a slow client is disconnected and re-reads the snapshot.","Before writing, wall asks the cooldown: strictly one pixel per COOLDOWN_SEC per session and no more than IP_BURST in the same sliding window per IP; a banned session or IP gets 403, an excess 429.","A single mutex orders the write to the canvas, the journal and the delta queue; a pixel takes 4 bits (an even index in the high nibble of the byte), 256x256 = 32 KiB.","Each placement is appended to the history (up to MAX_HISTORY records) with the old and new color; the client rolls the timelapse back from the current state, so no base snapshot is needed.","The canvas.snap snapshot is written every SNAPSHOT_SEC to a temp file with crc32, fsync and rename; recovery takes the snapshot, then the journal records with a larger seq, and truncates a torn tail.","journal.log (10 bytes per record: time, x, y, old and new color, clear and bot flags) is appended on every pixel with an fsync every 30 seconds; when MAX_HISTORY is exceeded by 25% the journal is rewritten with its tail.",[839,842,845,848],{"title":840,"text":841},"WebSocket instead of SSE and POST","A bidirectional binary channel is needed: a pixel in 6 bytes with an immediate ok or err, and binary deltas. SSE is text (base64 inflates traffic by a third) plus a separate POST per action. A fallback without a socket exists: POST \u002Fapi\u002Fpixel.",{"title":843,"text":844},"A snapshot with seq and deltas with firstSeq","A client registers with the hub and under the same lock gets a snapshot with its seq. A delta carries firstSeq: the client skips records with seq not above its own and reconnects on a gap.",{"title":846,"text":847},"The journal stores the old color","A record \"old\u003C\u003C4 | new\" lets the client roll history back from the current state, so no base snapshot is needed for the timelapse, and truncating the journal only shortens the history depth.",{"title":849,"text":850},"A snapshot and a journal instead of one file","A snapshot through a temp file and rename guarantees integrity, and the journal gives the latest changes; a corrupt snapshot (crc) is rebuilt from the journal, and a journal that does not join the snapshot starts over.",[852,853],"Everything is in one process on one disk; cooldowns, sessions and rate limits are in memory and reset on restart. A session is an identifier in localStorage, so it guards against simple bots, not an attack from many IPs.","The journal has no fsync per pixel (an fsync every 30 s), it is held whole in memory, there are no WebSocket extensions, and the client needs DecompressionStream. There are no browser tests in the repository.","Cooldown (session, IP, sliding window, table overflow), packing and boundary coordinates, codecs and broken messages, storage (crc, torn tail), recovery with no snapshot and from a snapshot plus a journal, compaction, history rollback, delta continuity, the API (limits, Origin, 401\u002F403\u002F429), WebSocket with two clients and flooding, admin rights, bans, a load of 300 clients; go test -race in CI.","README of the pixel-wall project (in Russian), sections \"Возможности\" (Features), \"Архитектура\" (Architecture), \"Бинарный протокол\" (Binary protocol), \"Формат хранения\" (Storage format), \"Ограничения\" (Limitations)",{"slug":791,"kind":90,"go_lines":857,"go_test_lines":858,"tests":859,"endpoints":860,"html":861,"css":551,"js":862},2715,1910,77,16,106,625,{"slug":864,"title":865,"stack":866,"summary":873,"nodes":874,"edges":902,"flows":911,"decisions":920,"tradeoffs":933,"tests":936,"source":937,"stats":938},"gitpulse","GitPulse: git repository analytics",[13,397,867,868,869,870,871,872,404,18],"CLI","git","html\u002Ftemplate","SVG","ANSI","os\u002Fexec","A CLI for analyzing git repositories: a git log stream parser, metric aggregators and three outputs from one structure: the terminal, a self-contained HTML and a local server. The point is not to write an own .git parser and to trust git with the history and renames.",[875,878,881,884,887,890,893,896,899],{"id":876,"label":877,"row":24,"col":24,"span":25,"kind":26},"cli","Command line\nfour commands and flags",{"id":879,"label":880,"row":24,"col":25,"span":25,"kind":35},"repo","Repository\n.git · .mailmap",{"id":882,"label":883,"row":25,"col":24,"span":34,"kind":30},"gitlog","gitlog (os\u002Fexec)\ngit log -z --numstat -M · streaming parser",{"id":885,"label":886,"row":34,"col":24,"span":34,"kind":30},"agg","analyze.Aggregator\ncommit by commit · renames · lock\u002Fvendor filter",{"id":888,"label":889,"row":45,"col":24,"span":34,"kind":30},"res","analyze.Result\none structure · JSON",{"id":891,"label":892,"row":54,"col":24,"span":25,"kind":30},"term","term\nANSI tables · sparklines",{"id":894,"label":895,"row":54,"col":25,"span":25,"kind":30},"report","report\nhtml\u002Ftemplate · inline SVG",{"id":897,"label":898,"row":56,"col":24,"span":25,"kind":26},"file","report.html\na single file or stdout",{"id":900,"label":901,"row":56,"col":25,"span":25,"kind":26},"serve","serve\n127.0.0.1 · Host guard",[903,904,905,906,907,908,909,910],{"n":25,"from":876,"to":882},{"n":34,"from":879,"to":882},{"n":45,"from":882,"to":885},{"n":54,"from":885,"to":888},{"n":56,"from":888,"to":891},{"n":59,"from":888,"to":894},{"n":61,"from":894,"to":897},{"n":136,"from":894,"to":900},[912,913,914,915,916,917,918,919],"The command parses the flags (-rev, -since, -until, -max-commits, -top, -depth, -json, -color) and checks the repository and revision; no git, not a repository or an unknown revision gives exit code 3, bad arguments 2.","git reads the history and .mailmap itself; author names therefore arrive already resolved through %aN\u002F%aE, and git -M finds the renames, with no own packfile parser.","gitlog runs git log -z --numstat -M through os\u002Fexec and parses the stream one record at a time: -z removes quoting and Unicode problems in paths, one commit lives in memory; progress is drawn to stderr on a terminal.","The Aggregator takes commits from newest to oldest and accumulates metrics; on meeting old => new it remembers the rename (resolving chains), so earlier edits of old land on the current name; lock files, vendor and min files are skipped, merges are counted separately.","The result is a single analyze.Result structure (JSON with -json); term draws tables with colors, bars and sparklines from it, NO_COLOR and output to a pipe or file switch the color off, and on Windows VT and UTF-8 are enabled.","report builds HTML through html\u002Ftemplate with inline SVG charts, CSS and a small theme-switch JS; there are no external requests, names and paths are escaped, and the theme follows prefers-color-scheme.","report writes a single file (-o file, gitpulse-report.html by default) or stdout with -o -.","serve accepts only a loopback address in -addr, rejects requests with a foreign Host header (DNS rebinding protection), sets CSP and no-store, keeps the result cached (30 s by default, \u002F?refresh=1 resets it) and serves \u002F, \u002Fapi\u002Fstats and \u002Fhealthz.",[921,924,927,930],{"title":922,"text":923},"git through os\u002Fexec, not a .git parser","An own parser of objects and packfiles with delta chains would also need the commit-graph, .mailmap, replace refs, shallow and partial clones and rename detection: thousands of lines with a risk of wrong metrics. git log --numstat -M solves this reliably; the price is a dependency on an installed git, which is checked explicitly.",{"title":925,"text":926},"A -z stream instead of loading the whole log","The log is read one record at a time, memory grows with the number of distinct files and authors, not commits; -max-commits marks the report as truncated.",{"title":928,"text":929},"One Result structure for every output","The terminal, HTML, JSON and the server show numbers from a single analyze.Result, so there is nothing to diverge, and the tests check the metrics in one place.",{"title":931,"text":932},"A self-contained HTML and a loopback server","The report contains author names, so it is a single file with no external requests or fonts, and serve listens only on 127.0.0.1 and checks Host.",[934,935],"Git must be installed; time is the author's UTC; merges are not analyzed; file copies are not tracked and renames are seen only where git -M finds them.","Language growth is an estimate of \"added minus removed\", and bus factor and risk are heuristics over line counts; memory grows with the number of files and authors, -json includes author e-mails and the HTML does not.","Synthetic repositories built through git and metrics on precomputed data: merges, renames (including an edit by the new path), binary files, Cyrillic in paths and names, lock files, an empty repository, a single commit, the commit limit, ranges, bad revisions; stream parsing (a tab in a path, broken records), a golden test of the HTML structure, self-containment and escaping of the report, the server (cache, Host guard, methods), exit codes, the CLI JSON, NO_COLOR; go test -race in CI.","README of the gitpulse project (in Russian), sections \"Команды\" (Commands), \"Что считается\" (What is counted), \"Архитектура\" (Architecture), \"HTML-отчёт\" (HTML report), \"Тесты\" (Tests), \"Ограничения\" (Limitations); code in internal\u002Fgitlog, internal\u002Fanalyze, internal\u002Freport, internal\u002Fterm, internal\u002Fserve",{"slug":864,"kind":90,"go_lines":939,"go_test_lines":940,"tests":549,"endpoints":45,"html":24,"css":24,"js":24},2133,735,1792697892477]