<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HS Solution</title>
    <description>The latest articles on DEV Community by HS Solution (@hssolution).</description>
    <link>https://dev.to/hssolution</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186887%2Ff0957f18-0fab-410d-9485-c244f5b2d8d7.jpg</url>
      <title>DEV Community: HS Solution</title>
      <link>https://dev.to/hssolution</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hssolution"/>
    <language>en</language>
    <item>
      <title>Adding Chinese to a pixel-font Godot game: 4 things that broke</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:43:16 +0000</pubDate>
      <link>https://dev.to/hssolution/adding-chinese-to-a-pixel-font-godot-game-4-things-that-broke-34eg</link>
      <guid>https://dev.to/hssolution/adding-chinese-to-a-pixel-font-godot-game-4-things-that-broke-34eg</guid>
      <description>&lt;p&gt;We make &lt;strong&gt;Rapid Miner&lt;/strong&gt;, a 2D pixel mining game in Godot where you dig, fight and build a village underground. Its UI text uses Galmuri9, a Korean pixel font. Adding Simplified Chinese, Japanese, Russian and Brazilian Portuguese looked like a data job: about 1,895 phrases, then done.&lt;/p&gt;

&lt;p&gt;The translation file was the easy part. These are the four things that broke, and what fixed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The font fallback looked wrong, and scaling did not fix it
&lt;/h2&gt;

&lt;p&gt;Galmuri9 has no Chinese glyphs, so we added Fusion Pixel (OFL-1.1) to its fallback list in the import settings. Our first pick was the 12px zh_hans cut.&lt;/p&gt;

&lt;p&gt;It rendered. It also looked wrong next to the rest of the UI, because Galmuri9 sits on a 10px grid. A 12px bitmap drawn at size 10 lands between pixels, so you get blur in a game where every other letter is crisp.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxvewgwyc01x3ykqyff4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxvewgwyc01x3ykqyff4.jpg" alt="The same three words from the game, drawn with the 12px and the 10px cut of Fusion Pixel at size 10, zoomed 10x" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We tried the obvious shortcut first: a &lt;code&gt;FontVariation&lt;/code&gt; with 1.2x scale. It made things worse. The glyphs grew but the advance width did not, so characters overlapped.&lt;/p&gt;

&lt;p&gt;The real fix was to pick the cut that matches the grid. We measured the share of mid-grey pixels in rendered text:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cut&lt;/th&gt;
&lt;th&gt;Size 10&lt;/th&gt;
&lt;th&gt;Size 20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fusion Pixel 12px&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fusion Pixel 10px&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fallback order in &lt;code&gt;Galmuri9.ttf.import&lt;/code&gt; is now the 10px cut first, then the 12px cut as a backup. Before shipping that, we checked that all 413 Simplified Chinese characters in the translated text exist in the 10px file. The rule we took away: &lt;strong&gt;a pixel font fallback has to share the grid of the main font. Measure grey pixels, do not trust your eyes at 100% zoom.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Changing language did not change the screen
&lt;/h2&gt;

&lt;p&gt;Our settings menu called &lt;code&gt;TranslationServer.set_locale()&lt;/code&gt; and stopped there. Nothing listened. Panels already on screen kept the old language until you reopened them.&lt;/p&gt;

&lt;p&gt;Now there is one path, and every screen goes through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;set_language&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;current_language&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;TranslationServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_locale&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;lang_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;current_language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lang_code&lt;/span&gt;
    &lt;span class="n"&gt;TranslationServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;save_settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;language_changed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upgrade menu, crafting tab, shop, pocket menu and boards connect to &lt;code&gt;language_changed&lt;/code&gt; and rewrite their own labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The fix created a new bug on icon buttons
&lt;/h2&gt;

&lt;p&gt;Our shop reroll button is icon-only. A small component clears its text so only the icon shows. Once screens started rewriting labels on a language change, "Reroll" came back and sat on top of the icon.&lt;/p&gt;

&lt;p&gt;The component now listens to the same signal and clears the text again on the next frame with &lt;code&gt;call_deferred&lt;/code&gt;, after the button's own code has run. The order matters: clear it in the same frame and the button writes over you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva1ex6za1vbtt6klqf7c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva1ex6za1vbtt6klqf7c.jpg" alt="Rapid Miner running in Simplified Chinese (development build)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Half-translated strings are worse than English
&lt;/h2&gt;

&lt;p&gt;Some of our strings are built in code with Korean and English side by side. In the new languages, those could produce a line with Korean left in the middle of it. Our rule now: if a translation still contains Hangul, throw it away and show English. A clean English line reads better than a mixed one.&lt;/p&gt;

&lt;p&gt;We also stopped checking this by hand. A &lt;code&gt;verify_lang_switch&lt;/code&gt; tool drives the real settings dropdown (&lt;code&gt;select()&lt;/code&gt; plus &lt;code&gt;item_selected&lt;/code&gt;), waits two frames, then walks the visible screens and counts leftover Hangul and leftover English. It also checks the shape of the reroll button, because that bug taught us it can come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do first next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check the grid size of your font before choosing a fallback.&lt;/li&gt;
&lt;li&gt;Build the single &lt;code&gt;set_language&lt;/code&gt; path before adding any language.&lt;/li&gt;
&lt;li&gt;Write the checker before the translations, so you can count what is left.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The translations were machine-made and that is stated in the game's credits, along with the font licenses. The four new languages are in a development build for now. The public demo is English and Korean.&lt;/p&gt;

&lt;p&gt;If you have shipped a pixel-font game in Chinese or Japanese, what did you do about mixed grids?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://store.steampowered.com/app/4449480/?utm_source=devto&amp;amp;utm_campaign=g-1141" rel="noopener noreferrer"&gt;Rapid Miner on Steam&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>localization</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Gluing leather right in a leathercraft game (Godot devlog)</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Fri, 30 Oct 2026 17:41:49 +0000</pubDate>
      <link>https://dev.to/hssolution/gluing-leather-right-in-a-leathercraft-game-godot-devlog-12l2</link>
      <guid>https://dev.to/hssolution/gluing-leather-right-in-a-leathercraft-game-godot-devlog-12l2</guid>
      <description>&lt;p&gt;I make &lt;strong&gt;Leather Atelier&lt;/strong&gt;, a cozy leathercraft game in Godot 4 where you cut, stitch and finish wallets and bags for customers. This week I rebuilt the gluing step. The old version was "brush glue, press, done". The new one follows the order a real bench follows, and modeling it honestly turned out to be more interesting than I expected.&lt;/p&gt;

&lt;p&gt;Here is what changed, and the small rules that made it feel right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order, as a state machine
&lt;/h2&gt;

&lt;p&gt;The gluing step is now a fixed sequence. Each stage has to finish before the next one unlocks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mark the band.&lt;/strong&gt; Scribe the strip that will be glued with an awl, so glue stays inside the line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rough the grain side only.&lt;/strong&gt; The smooth grain side gets scuffed. The rough flesh side skips this step, because it already grips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coat both faces.&lt;/strong&gt; Brush glue on both pieces. Each face gets its own drying timer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check tack with a fingertip.&lt;/strong&gt; For contact cement, you touch both faces. If nothing transfers to your finger, they are ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lift and lay.&lt;/strong&gt; Pick up the piece and lower it from one end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll, clean, sand.&lt;/strong&gt; Press with a roller, wipe the squeeze-out, then sand the edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F510r4ufsdlf8d4pwxq0o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F510r4ufsdlf8d4pwxq0o.jpg" alt="Coating the inner faces of a pocket (dev build, Korean UI)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two glue types, two different rules
&lt;/h2&gt;

&lt;p&gt;The part I got wrong at first: not all glue works the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contact cement:&lt;/strong&gt; coat both faces, wait, then join. Once they touch, they grab. So the game makes you test tack &lt;em&gt;before&lt;/em&gt; joining, and you cannot slide the piece afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wet-bond glue:&lt;/strong&gt; join first, press, &lt;em&gt;then&lt;/em&gt; let it dry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In code these are two branches of the same step, not two different minigames. The difference is only &lt;em&gt;when&lt;/em&gt; the drying timer runs: before the join, or after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 1 mm mistake stays with you
&lt;/h2&gt;

&lt;p&gt;An inner pocket is worked separately from the body panel: both faces coated, then the same pocket is picked up and laid down from one end. If you place it 1 mm off, the game keeps that offset. It shows up later, when the stitching chisel path runs along the edge.&lt;/p&gt;

&lt;p&gt;I like this rule a lot. It makes gluing matter without adding a fail screen. You just see your own mistake again two steps later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stacking layers on one wall
&lt;/h2&gt;

&lt;p&gt;A card wallet wall can hold interfacing, a base board, lining and card slots, all on the same face. Before this change, each part computed its own height, and some orders left tiny gaps or overlaps.&lt;/p&gt;

&lt;p&gt;Now one height model computes the whole stack. Whatever order you glue in, and even after saving and reloading mid-step, adjacent layers meet with &lt;strong&gt;0 ±0.05 mm&lt;/strong&gt; gap. The test suite runs 24 + 8 stacking combinations for this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgl3trmrf9cczqvth6lq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgl3trmrf9cczqvth6lq.jpg" alt="Wiping dry squeeze-out before sanding the edge (dev build, Korean UI)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No hands on the work screen.&lt;/strong&gt; Tools follow your input, and leather is lifted and carried without a drawn hand. Floating hands kept covering the exact spot you needed to see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stall when coming back from the sewing machine.&lt;/strong&gt; Returning to the glue step used to freeze for &lt;strong&gt;886 ms&lt;/strong&gt; while it rebuilt prep state. It now takes &lt;strong&gt;99 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Most of the "feel" came from ordering, not from effects. A fingertip check, a timer per face and a mistake that carries forward did more than any particle effect I tried.&lt;/p&gt;

&lt;p&gt;The screenshots above are from a Korean-language dev build. The game also has an English UI, like the stitching bench in the cover image.&lt;/p&gt;

&lt;p&gt;Leather Atelier is on Steam as "Coming soon" (no release date yet). If you like slow, hands-on crafting games, a wishlist helps a lot: &lt;a href="https://store.steampowered.com/app/5412510/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=bag_glue_1031" rel="noopener noreferrer"&gt;Leather Atelier on Steam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have glued leather yourself, I would like to know which step I still have wrong.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>devlog</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Mac Screen Sharing slow from Windows? What to check, and why Windows has no fast mode</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:12:00 +0000</pubDate>
      <link>https://dev.to/hssolution/mac-screen-sharing-slow-from-windows-what-to-check-and-why-windows-has-no-fast-mode-4ep1</link>
      <guid>https://dev.to/hssolution/mac-screen-sharing-slow-from-windows-what-to-check-and-why-windows-has-no-fast-mode-4ep1</guid>
      <description>&lt;p&gt;If you connect to a Mac from a Windows PC with a VNC viewer and it feels sluggish, a few causes are quick to fix. One is not: it comes from how Windows has to connect. Here is what I check, in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The basics (try these first)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer pixels.&lt;/strong&gt; Lower the Mac's display resolution, or make the viewer window smaller. This is the fastest change you can make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wired if possible.&lt;/strong&gt; Put the Mac and the PC on Ethernet instead of Wi-Fi, and keep both on the same network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quiet Mac.&lt;/strong&gt; Close heavy apps while connected. Check that the Mac is not in the middle of a Time Machine backup or a software update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of these fixes it, stop here. You do not need anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why Windows has no "fast mode"
&lt;/h2&gt;

&lt;p&gt;Two facts from Apple's own Remote Desktop documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For non-Apple VNC viewers, you have to turn on &lt;strong&gt;"VNC viewers may control screen with password"&lt;/strong&gt;. Apple also notes that third-party viewers may not encrypt keystrokes.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;High Performance&lt;/strong&gt; Screen Sharing mode only works between Apple silicon Macs on macOS Sonoma 14 or later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a Windows PC always stays on the standard VNC path. That does not prove VNC is why &lt;em&gt;your&lt;/em&gt; connection is slow (network and resolution matter more often). It does mean there is no faster mode to switch on from the Windows side.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Changing the protocol instead
&lt;/h2&gt;

&lt;p&gt;Windows already ships with an RDP client, Remote Desktop Connection (&lt;code&gt;mstsc.exe&lt;/code&gt;). macOS has no RDP server, so &lt;code&gt;mstsc&lt;/code&gt; cannot reach a Mac out of the box.&lt;/p&gt;

&lt;p&gt;I work on a small Mac app, RDPyon, that adds an RDP server to the Mac. You install it on the Mac and connect with &lt;code&gt;mstsc&lt;/code&gt; from Windows. Nothing gets installed on the Windows side.&lt;/p&gt;

&lt;p&gt;To be clear about what I have &lt;em&gt;not&lt;/em&gt; done: I have not benchmarked it against Screen Sharing, and I cannot promise it is faster on your network. What changes is the protocol (RDP instead of VNC) and the client (the built-in Windows one instead of a VNC viewer). The free trial is there so you can compare on your own setup.&lt;/p&gt;

&lt;p&gt;Limits worth knowing before you try it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;12.3 or later (Mac audio forwarding needs 13+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login window&lt;/td&gt;
&lt;td&gt;Not supported. The Mac must already be logged in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;One viewer, one display at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipboard&lt;/td&gt;
&lt;td&gt;Text and images work. File copy is not guaranteed yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Same LAN or a VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full checklist, with the Apple sources, is here: &lt;a href="https://rdpyon.com/guide/mac-screen-sharing-slow-from-windows/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=rdpyon_slow_screen_sharing" rel="noopener noreferrer"&gt;Mac Screen Sharing slow from Windows: what to try&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a fix that worked for you on the VNC side, I would like to hear it in the comments. Most slow sessions I have seen were solved by step 1.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: Apple Remote Desktop User Guide ("Allow access using VNC") and "Use high-performance screen sharing" (checked October 2026). macOS and Screen Sharing are trademarks of Apple. HS Solution is not affiliated with Apple or Microsoft.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>windows</category>
      <category>remotedesktop</category>
      <category>vnc</category>
    </item>
    <item>
      <title>How Korean naming houses use birth elements, and how I built a simplified version in the browser</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:26:27 +0000</pubDate>
      <link>https://dev.to/hssolution/how-korean-naming-houses-use-birth-elements-and-how-i-built-a-simplified-version-in-the-browser-51o4</link>
      <guid>https://dev.to/hssolution/how-korean-naming-houses-use-birth-elements-and-how-i-built-a-simplified-version-in-the-browser-51o4</guid>
      <description>&lt;p&gt;I'm the developer of this small free tool, so I'll say that up front. It is a Korean name generator that reads the Five Elements in a birthday and suggests three names with Hanja. This post covers the naming idea behind it and the parts that were fun to build. It is for entertainment, and it is a simplified version of the traditional practice.&lt;/p&gt;

&lt;p&gt;Try it here if you want to follow along: &lt;a href="https://devjar.dev/lab/korean-name/?utm_source=devto&amp;amp;utm_campaign=korean-name-en" rel="noopener noreferrer"&gt;Korean Name Generator&lt;/a&gt;. It is free, needs no sign-up, and runs in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  How naming houses use birth elements
&lt;/h2&gt;

&lt;p&gt;In Korea, some families visit a naming house (작명소) before choosing a baby's name. The people there start from the baby's saju (사주), the "four pillars" of birth year, month, day and hour. Each pillar is two characters, a heavenly stem and an earthly branch, and every character maps to one of five elements: wood, fire, earth, metal, water.&lt;/p&gt;

&lt;p&gt;Count the elements across the pillars and you usually get an uneven spread. One element may be absent or thin. The common idea is to pick a name whose characters carry that thin element, so the name "fills the gap".&lt;/p&gt;

&lt;p&gt;Names in Korea are usually written in Hangul but also have a Hanja spelling. Each Hanja carries a meaning, and many have a radical that points to an element. 林 (forest) has the wood radical. 江 (river) has the water radical. 鎬 (bright) has the metal radical. This is called 자원오행 (resource element). So the loop is simple to state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute the pillars from the birthday.&lt;/li&gt;
&lt;li&gt;Count the five elements.&lt;/li&gt;
&lt;li&gt;Find the weak one.&lt;/li&gt;
&lt;li&gt;Choose Hanja whose radical belongs to that element.&lt;/li&gt;
&lt;li&gt;Check that the sound also works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Naming houses weigh many more things: strokes, the family's generation character, the parents' wishes, and a judgment about which element helps the person most (용신). I do none of that last part. I only fill the thinnest element, and the page says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Counting the elements
&lt;/h2&gt;

&lt;p&gt;The calendar math (converting a Gregorian date to pillars) is shared with another small tool of mine, so the name logic starts from the counts. If the birth hour is unknown, the hour pillar is empty and the count uses six characters instead of eight.&lt;/p&gt;

&lt;p&gt;The weak-element rule is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Missing element = an element counted 0; otherwise the smallest count.
   Ties: first the element that feeds the Day Master (인성), then wood→fire→earth→metal→water. */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;missingElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;dmElem&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="nx"&gt;tied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;tied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;mom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;parentOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dmElem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="nx"&gt;mom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;tied&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tie-break matters because ties happen often with only six to eight characters. I let the element that feeds the Day Master win, since that is a common reading of what supports the person, and then a fixed order so the answer is stable. This is a design decision, not a ground truth. The result page shows the five counts as bars so you can see what the tool saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Unknown birth time
&lt;/h2&gt;

&lt;p&gt;Most people outside Korea do not know their birth hour. I made "unknown" the default instead of forcing a guess. With the hour missing, the tool counts six characters and shows a small badge: adding the hour can change the result. That is honest about what the input supports, and it keeps the form short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Sound elements, and where schools disagree
&lt;/h2&gt;

&lt;p&gt;Hanja radicals give the "meaning element" of each character. Korean naming also looks at the initial consonant of each syllable, the 발음오행 (sound element). Here the schools do not agree.&lt;/p&gt;

&lt;p&gt;The mapping I use follows the 운해본 reading, the majority convention in naming practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ㄱ ㅋ: wood&lt;/li&gt;
&lt;li&gt;ㄴ ㄷ ㄹ ㅌ: fire&lt;/li&gt;
&lt;li&gt;ㅇ ㅎ: earth&lt;/li&gt;
&lt;li&gt;ㅅ ㅈ ㅊ: metal&lt;/li&gt;
&lt;li&gt;ㅁ ㅂ ㅍ: water&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 해례본 reading swaps ㅇ ㅎ and ㅁ ㅂ. I did not try to settle that. I picked the majority convention, and the About section of the page states that another reading exists. If you know this area better than I do, I would like to hear which side you would pick and why.&lt;/p&gt;

&lt;p&gt;In code the table is a pair of arrays indexed by the Hangul initial, decoded from the Unicode syllable block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;CHO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄱ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄲ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄴ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄷ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄸ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㄹ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅁ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅂ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅃ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅅ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅆ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅇ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅈ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅉ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅊ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅋ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅌ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅍ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ㅎ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;CHO_ELEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jamo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;0xAC00&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;cho&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;588&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="na"&gt;jung&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;588&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="na"&gt;jong&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;};}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Hangul syllable is &lt;code&gt;0xAC00 + (initial * 588) + (vowel * 28) + final&lt;/code&gt;, so no lookup table of syllables is needed. The tool then prefers name pairs where neighboring sounds feed each other (wood to fire to earth to metal to water) and penalizes pairs where one controls the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: How I chose the Hanja table
&lt;/h2&gt;

&lt;p&gt;A name generator is only as good as its character list, and Hanja is a large field. I wanted a small table I can defend instead of a scrape.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: the Unicode Unihan database (version 18.0.0), which is public. I used the radical field (&lt;code&gt;kRSUnicode&lt;/code&gt;) for the element, the Korean reading field (&lt;code&gt;kHangul&lt;/code&gt;) for the sound, and &lt;code&gt;kDefinition&lt;/code&gt; as a reference for English meanings.&lt;/li&gt;
&lt;li&gt;Name-use filter: Korea restricts which Hanja may be used in registered names. Unihan marks these in &lt;code&gt;kKoreanName&lt;/code&gt; (additional characters, by announcement year) and &lt;code&gt;kKoreanEducationHanja&lt;/code&gt; (the 1,800 education characters). I only kept characters found in one of those two fields.&lt;/li&gt;
&lt;li&gt;Radicals only when clear: wood (木, 艹, 竹, 禾), fire (火, 灬), earth (土, 山, 田), metal (金, 玉), water (水, 氵, 雨, 冫). Radicals like 日, 石 and 扌 are argued over, so I left them out instead of picking a side quietly.&lt;/li&gt;
&lt;li&gt;Common syllables first: I started from syllables people use in given names, which keeps results from looking odd.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final table has 264 characters: 55 wood, 33 fire, 38 earth, 72 metal, 66 water. Each row looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hanja&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;蓮&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hangul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;연&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;element&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;艹&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meaning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lotus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tags&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;basis&lt;/code&gt; is the radical, so the result card can say why a character was chosen. &lt;code&gt;tags&lt;/code&gt; marks a typical gender feel (F, M or N for neutral), which is how the optional "female / male / either" input filters the list. The file header says it is generated by a script, so I fix the candidate list there and regenerate instead of editing rows by hand.&lt;/p&gt;

&lt;p&gt;The English glosses are short and based on the Unihan definitions. Some are loose. If one looks wrong to you, tell me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Avoiding famous names
&lt;/h2&gt;

&lt;p&gt;If someone's birthday produces the full name of a well-known singer or actor, that is awkward and a bit misleading. So there is a list of widely known full names (names only, taken from public wikis, no photos or descriptions). Any surname plus given-name combination that matches the list is skipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isFamous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Same birthday, same names
&lt;/h2&gt;

&lt;p&gt;Random output has a problem for sharing: refresh and the names change. I wanted the same birthday to give the same three names every time, so the generator seeds its random numbers from the input. A hash turns the input string into a seed, and a small generator makes the sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;rnd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;seedOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surname&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;seedOf&lt;/code&gt; is FNV-1a and &lt;code&gt;rng&lt;/code&gt; is mulberry32, both a few lines. Candidates are shuffled with this seeded random source, then sorted by a score (common names and natural syllable positions score higher, sound clashes score lower), and each Hangul name keeps its highest-scoring Hanja pair. Because every step is deterministic, you and a friend with the same input see the same result. There is no hidden state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Nothing leaves the browser
&lt;/h2&gt;

&lt;p&gt;There is no server-side calculation. The pillars, the picking, the romanization and the share-card drawing all run in your browser. The birthday is not put in the URL, local storage or cookies. The page does send one small anonymous beacon with an event name (such as "view") and the referrer, so I can see whether anyone opens it. The birthday is not part of it.&lt;/p&gt;

&lt;p&gt;Romanization follows the Revised Romanization of Korean for given names, syllable by syllable, with a hyphen only where letters could be misread. Surnames use the common spellings (Kim, Lee, Park) instead of the formal forms (Gim, I, Bak), because that is how people write them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is for entertainment. It is a simplified version of Korean naming practice, not professional advice.&lt;/li&gt;
&lt;li&gt;It does not judge which element suits a person most. It fills the thinnest one.&lt;/li&gt;
&lt;li&gt;Sound-element schools differ, and I used one of them.&lt;/li&gt;
&lt;li&gt;The Hanja table is small on purpose, so many valid names are not in it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page is available in English, Japanese, Spanish, Portuguese and Indonesian, using the same engine and table.&lt;/p&gt;

&lt;p&gt;If you want to look at your own result, here is the tool again: &lt;a href="https://devjar.dev/lab/korean-name/?utm_source=devto&amp;amp;utm_campaign=korean-name-en" rel="noopener noreferrer"&gt;devjar.dev/lab/korean-name&lt;/a&gt;. Feedback on the character meanings or the sound-element choice is welcome in the comments.&lt;/p&gt;

&lt;p&gt;I also make a saju app for Android: &lt;a href="https://play.google.com/store/apps/details?id=com.noligo.saju_app&amp;amp;utm_source=devto&amp;amp;utm_campaign=korean-name-en" rel="noopener noreferrer"&gt;Starlit Alley&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post was written with AI assistance.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Connecting from Windows to a Mac with the built-in Remote Desktop (mstsc)</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Fri, 23 Oct 2026 01:54:21 +0000</pubDate>
      <link>https://dev.to/hssolution/connecting-from-windows-to-a-mac-with-the-built-in-remote-desktop-mstsc-1d29</link>
      <guid>https://dev.to/hssolution/connecting-from-windows-to-a-mac-with-the-built-in-remote-desktop-mstsc-1d29</guid>
      <description>&lt;p&gt;When I need to reach a Mac from a Windows PC, the usual advice is to install a VNC server on the Mac and a separate client on Windows. That works, but it is one more program to install and explain.&lt;/p&gt;

&lt;p&gt;Windows already ships with a remote desktop client, &lt;code&gt;mstsc.exe&lt;/code&gt;. It speaks RDP, and macOS has no RDP server. So I built one: RDPyon is a Mac app that accepts RDP connections, so you can use &lt;code&gt;mstsc&lt;/code&gt; and install nothing on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Mac on macOS 12.3 (Monterey) or later, Apple Silicon or Intel.&lt;/li&gt;
&lt;li&gt;You are logged in on the Mac. You sign in with the Mac owner's short account name and its password.&lt;/li&gt;
&lt;li&gt;A network path between the two machines: same network, or a VPN.&lt;/li&gt;
&lt;li&gt;An RDPyon license key, or the 14-day free trial (no card required).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install RDPyon on the Mac.&lt;/strong&gt; Open the &lt;code&gt;.dmg&lt;/code&gt;, drag RDPyon into Applications, and open it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Allow the permissions.&lt;/strong&gt; Allow what the app asks for on first launch (Screen Recording, Accessibility) in System Settings under Privacy &amp;amp; Security, then restart the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Find the Mac's address.&lt;/strong&gt; Click the RDPyon menu bar icon. It shows "Local IP" and "Port". The default port is 3389; "Change Server Port" accepts 1024 to 65535.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Open Remote Desktop Connection.&lt;/strong&gt; Press Windows key + R and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;mstsc /v:MacAddress:Port /prompt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Sign in with your Mac account.&lt;/strong&gt; Choose "More choices" or "Use a different account", then enter the short account name and your Mac login password. An empty password is rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Check the connection.&lt;/strong&gt; When the Mac screen appears in the window, you are in. A connection bar shows at the top of the Mac screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;The built-in Windows Remote Desktop Connection (mstsc.exe). Nothing to install on Windows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported Macs&lt;/td&gt;
&lt;td&gt;macOS 12.3 or later. Both Apple Silicon and Intel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign-in&lt;/td&gt;
&lt;td&gt;Your Mac account's short name and password. The connection uses TLS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen size&lt;/td&gt;
&lt;td&gt;The Mac screen is sent at the size of the Remote Desktop window. How well a window resize is followed can vary by client version and settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipboard&lt;/td&gt;
&lt;td&gt;Copy and paste text and images between the Mac and Windows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound&lt;/td&gt;
&lt;td&gt;Sends the Mac's sound to Windows. Needs macOS 13 or later. Turn on "Forward Mac Audio" in the menu; it is off by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shortcuts&lt;/td&gt;
&lt;td&gt;Windows Ctrl is sent as Mac Cmd, and the Windows key as Mac Ctrl. Ctrl+C and Ctrl+V work as you expect.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;Default 3389. You can change it to any value from 1024 to 65535.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Choosing a display&lt;/td&gt;
&lt;td&gt;If your Mac has several displays, pick the one to send with "Select Monitor" in the menu.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What does not work
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NLA (Network Level Authentication)&lt;/td&gt;
&lt;td&gt;Not provided. The Mac account is checked over a TLS connection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side sign-in screen&lt;/td&gt;
&lt;td&gt;None. You enter your credentials in the Remote Desktop Connection window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simultaneous connections&lt;/td&gt;
&lt;td&gt;One session at a time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple displays&lt;/td&gt;
&lt;td&gt;One display is sent at a time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed sign-ins&lt;/td&gt;
&lt;td&gt;After 5 failed attempts in a row from the same IP address, connections are blocked for 30 seconds. The block gets longer with each further failure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Korean/English key and Hanja key&lt;/td&gt;
&lt;td&gt;The Korean/English key switches the Mac input source, and the Hanja key is sent as Option+Return. A keyboard that uses Right Alt as the Korean/English key cannot be told apart from Option. mstsc may also capture the Windows key, Ctrl, or CapsLock first, so behavior can differ by setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locking local input while connected&lt;/td&gt;
&lt;td&gt;Not available yet (the menu item is marked "coming soon").&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac screen lock&lt;/td&gt;
&lt;td&gt;If the screen is locked, connecting may fail, so unlock the Mac first. If the screen locks during a session, the connection ends by default ("Disconnect when screen locks" is on).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copying and pasting files&lt;/td&gt;
&lt;td&gt;Not guaranteed yet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac App Store&lt;/td&gt;
&lt;td&gt;RDPyon is not distributed through the App Store. You download and install it directly.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Compatibility checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mac&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] macOS 12.3 or later.&lt;/li&gt;
&lt;li&gt;[ ] You are logged in on the Mac.&lt;/li&gt;
&lt;li&gt;[ ] Screen Recording and Accessibility are allowed, and the app was restarted afterwards.&lt;/li&gt;
&lt;li&gt;[ ] The Mac screen is unlocked (connecting to a locked Mac may fail, and the session ends on lock by default).&lt;/li&gt;
&lt;li&gt;[ ] For sound: macOS 13 or later, with "Forward Mac Audio" on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network and port&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Both machines are on the same network, or on a VPN.&lt;/li&gt;
&lt;li&gt;[ ] You use the Local IP and Port shown in the RDPyon menu.&lt;/li&gt;
&lt;li&gt;[ ] The port in your &lt;code&gt;mstsc&lt;/code&gt; command matches the menu (default 3389, custom 1024 to 65535).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Windows and mstsc&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You use the built-in &lt;code&gt;mstsc.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] An &lt;code&gt;.rdp&lt;/code&gt; file does not contain &lt;code&gt;enablecredsspsupport:i:0&lt;/code&gt;. On Windows 10 that line stops mstsc from asking for TLS and the connection is blocked.&lt;/li&gt;
&lt;li&gt;[ ] You entered the short account name, not the full name, and a non-empty password.&lt;/li&gt;
&lt;li&gt;[ ] You have not failed 5 sign-ins in a row from the same IP. If so, wait 30 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Expectations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You need one session and one display.&lt;/li&gt;
&lt;li&gt;[ ] You do not rely on file copy and paste.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gaps you find are welcome in the comments.&lt;/p&gt;

&lt;p&gt;RDPyon costs $19 one-time (up to 2 Macs, 1 year of updates, 30-day refund), and the 14-day trial starts without a card. The full guide, with the same tables, is here: &lt;a href="https://rdpyon.com/guide/windows-to-mac-remote-desktop/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=rdpyon_launch" rel="noopener noreferrer"&gt;Windows to Mac remote desktop guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and checked against the product guide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>windows</category>
      <category>remotedesktop</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why our Flutter in-app purchases got stuck on "Loading" (and the retry design that fixed it)</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:08:58 +0000</pubDate>
      <link>https://dev.to/hssolution/why-our-flutter-in-app-purchases-got-stuck-on-loading-and-the-retry-design-that-fixed-it-21d8</link>
      <guid>https://dev.to/hssolution/why-our-flutter-in-app-purchases-got-stuck-on-loading-and-the-retry-design-that-fixed-it-21d8</guid>
      <description>&lt;p&gt;In our Flutter app (Android and iOS, &lt;code&gt;in_app_purchase&lt;/code&gt; 3.3.1) we shipped a bug where the shop's price list stayed on "Loading" forever. The cause was simple: if connecting to the store or querying products failed once, nothing ever tried again until the user restarted the app.&lt;/p&gt;

&lt;p&gt;Here is what was wrong, and the retry design we replaced it with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2F0sw30pLAe6U5hZELu6NCkE9z5Rz6Z-yBK3sAbkW5r9RyaI8kChvkkYL-TValVURssrDSCBRvAe_N6RQkhMZubQ%3Dw720" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2F0sw30pLAe6U5hZELu6NCkE9z5Rz6Z-yBK3sAbkW5r9RyaI8kChvkkYL-TValVURssrDSCBRvAe_N6RQkhMZubQ%3Dw720" alt="The app's home screen, from the Google Play listing" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was going on
&lt;/h2&gt;

&lt;p&gt;All billing code lives in a &lt;code&gt;Billing&lt;/code&gt; singleton with three states: &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;ready&lt;/code&gt; and &lt;code&gt;unavailable&lt;/code&gt;. The old &lt;code&gt;refresh()&lt;/code&gt; looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_gw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BillingStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not connected, or busy: drop it&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BillingStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BillingStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unavailable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// one failure ends it&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProductIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;_products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;found&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// full overwrite&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BillingStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unavailable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// no retry&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That has four holes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A failed connection at startup is permanent.&lt;/strong&gt; If creating the gateway threw at launch, &lt;code&gt;_gw&lt;/code&gt; stayed &lt;code&gt;null&lt;/code&gt;, and every later &lt;code&gt;refresh()&lt;/code&gt; returned on the first line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests during a query were dropped.&lt;/strong&gt; A refresh that arrived while &lt;code&gt;loading&lt;/code&gt; was simply ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No automatic retry.&lt;/strong&gt; &lt;code&gt;isAvailable() == false&lt;/code&gt;, a timeout and an exception all ended in &lt;code&gt;unavailable&lt;/code&gt;. The only retry was opening the shop screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial results overwrote good data.&lt;/strong&gt; A query that returned only some products could erase products we already had.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Temporary store failures are common: the Play Store app updating itself, a network blip. The real problem was that one failure meant "Loading" forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reconnect on the next attempt
&lt;/h3&gt;

&lt;p&gt;If the connection fails at startup we don't give up. &lt;code&gt;ensureStarted()&lt;/code&gt; reconnects on every new attempt, and syncs past purchases once it connects.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Don't drop requests during a query — queue one more
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_inflight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_again&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// run once more after this one&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However many times it is called, at most one extra query runs after the current one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Merge results instead of overwriting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;_products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;found&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// a partial answer can't erase known products&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Retry automatically at 5 s → 30 s → 2 min, then stop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;retryDelays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;minutes:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_afterRefresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;needsRetry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_retryStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// got everything: reset&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_retryStep&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retryDelays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// stop after 3&lt;/span&gt;
  &lt;span class="n"&gt;_retryTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryDelays&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_retryStep&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;unawaited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We cap it on purpose. On devices where the store really is unavailable (no Play Store, for example), retrying forever only burns battery and data. Timeouts stay at 15 s for the connection check and 20 s for the query.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Let user actions and app resume trigger a retry
&lt;/h3&gt;

&lt;p&gt;If the three automatic tries fail, four other things trigger a retry: app resume, opening the shop, pressing a buy button, and a manual retry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.dart&lt;/span&gt;
&lt;span class="n"&gt;AppLifecycleListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;onResume:&lt;/span&gt; &lt;span class="n"&gt;Billing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onResume&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// billing.dart&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onResume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needsRetry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;unawaited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryNow&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// only when products are missing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't listen to a connectivity stream. Timers handle the automatic part; everything else comes from the user or from resume. A manual trigger resets the retry schedule to zero. If the buy button is pressed while billing isn't ready, it calls &lt;code&gt;retryNow()&lt;/code&gt; and then continues straight into the purchase.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Stop saying "Loading"
&lt;/h3&gt;

&lt;p&gt;We split failures into &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;notFound&lt;/code&gt;, and the screen now says "Connecting to the store… / Retry" instead of "Loading". With "Loading", users can't tell whether waiting will help or whether the item is gone. The first time each failure type appears we log an analytics event, so we can see later which failures are common.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write billing setup so it can be redone any number of times, not "once at startup".&lt;/li&gt;
&lt;li&gt;Don't drop requests that arrive mid-query; queue exactly one more.&lt;/li&gt;
&lt;li&gt;Merge query results so a partial answer can't delete known products.&lt;/li&gt;
&lt;li&gt;Cap automatic retries; leave the rest to app resume and user actions.&lt;/li&gt;
&lt;li&gt;Don't show a vague state like "Loading" when you know what failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shipped in version 1.1.1 on Android and iOS. We haven't yet verified every real-payment path in production, so if your setup is similar, try it with a Play Console license tester: toggle airplane mode and force-stop the Play Store app while the shop is open.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm the developer of this app, **Starlit Alley: Tarot &amp;amp; Saju&lt;/em&gt;* (HS Solution). Core readings (saju, tarot, dream reading) are free, with no sign-up and no ads. &lt;a href="https://play.google.com/store/apps/details?id=com.noligo.saju_app&amp;amp;referrer=utm_source%3Ddevto%26utm_medium%3Darticle%26utm_campaign%3Dbilling-retry" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted by an AI agent from our repository and fix notes. The code and numbers are quoted from the actual repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>android</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Scope creep is not the problem. Unwritten change requests are.</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:17:38 +0000</pubDate>
      <link>https://dev.to/hssolution/scope-creep-is-not-the-problem-unwritten-change-requests-are-268m</link>
      <guid>https://dev.to/hssolution/scope-creep-is-not-the-problem-unwritten-change-requests-are-268m</guid>
      <description>&lt;p&gt;Most scope fights I have seen on client projects did not start with a big request. They started with a small one, accepted in a chat message ("sure, I'll try"), and nobody wrote down what it moved. Three weeks later the deadline has drifted and neither side can say which request caused it.&lt;/p&gt;

&lt;p&gt;The fix is boring and it works: every change after kickoff goes on a one-page change request with a number. Here are the seven fields we put on that page, from the developer side of the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven fields
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. What changes, in screen terms.&lt;/strong&gt; Not "improve search", but "add a date-range filter to the order list; picking a start and end date shows only orders in between." Test: ask someone new to describe the finished screen from your sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Why (one line).&lt;/strong&gt; "At month end the manager only wants last month's orders but has to scroll the whole list." Knowing the purpose is what lets you, the developer, say "there is a cheaper way."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What it touches.&lt;/strong&gt; Screens modified, queries modified, new screens, data-structure changes, integrations. This is where "it's just one field" turns into "that's a migration" — say it before work starts, not halfway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Effect on schedule, in dates.&lt;/strong&gt; "Interim demo moves 2 days; final delivery unchanged." Send the updated overall schedule with the same CR number, so schedule versions line up with requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Effect on cost, with a basis.&lt;/strong&gt; Not an amount pulled from the air — the basis (workload, agreed rate) and a reference to the quote. "To be discussed" is not an answer; don't start on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. What gets dropped or deferred.&lt;/strong&gt; If the client wants the date and budget unchanged, something else moves. Put the trade on the same page: "Admin Excel export moves to phase two." Check it isn't something they marked as core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Who approves, and how.&lt;/strong&gt; Both contacts reply "proceed" by email (or sign the doc). Work starts after that, not before. Number, date, approver name.&lt;/p&gt;

&lt;h2&gt;
  
  
  A filled-in (made-up) example
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number / date&lt;/td&gt;
&lt;td&gt;CR-003 · requested by client contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change&lt;/td&gt;
&lt;td&gt;Date-range filter on the order list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;See only last month's orders at month end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impact&lt;/td&gt;
&lt;td&gt;Order list screen, order query. No new screens, no schema change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;Interim demo +2 days; final delivery unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Extra; basis = 1 screen + 1 query change, amount per attached quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-off&lt;/td&gt;
&lt;td&gt;Admin Excel export deferred to phase two&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval&lt;/td&gt;
&lt;td&gt;Starts after both contacts approve by email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Not everything needs a CR
&lt;/h2&gt;

&lt;p&gt;Sort first. If something agreed in the requirements doesn't work, that's a defect — you fix it, no CR. Wording and color tweaks inside an agreed screen usually go in a batch. A new behavior, a new screen, or anything that touches data or integrations gets a number. Where exactly the line sits depends on your contract wording, so agree on the sorting rule at kickoff, not during the first argument.&lt;/p&gt;

&lt;p&gt;What I like about this as a developer: it turns "the client keeps adding stuff" into a list you can both point at. Saying yes gets cheaper because every yes has a price tag and a date next to it.&lt;/p&gt;

&lt;p&gt;The full guide (with the sorting table and how each field fails if you skip it) is here: &lt;a href="https://noligo.co.kr/en/guide/change-request-template/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=change-request-devto" rel="noopener noreferrer"&gt;How to write a change request&lt;/a&gt;. It's general information, not legal advice.&lt;/p&gt;

&lt;p&gt;How do you handle mid-project requests — a numbered doc, tickets, or just a running chat thread?&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>productivity</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Chat Folders for Claude: sidebar folders, and exporting a virtualised chat to Markdown</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Tue, 20 Oct 2026 11:45:20 +0000</pubDate>
      <link>https://dev.to/hssolution/chat-folders-for-claude-sidebar-folders-and-exporting-a-virtualised-chat-to-markdown-4khc</link>
      <guid>https://dev.to/hssolution/chat-folders-for-claude-sidebar-folders-and-exporting-a-virtualised-chat-to-markdown-4khc</guid>
      <description>&lt;p&gt;I use claude.ai a lot, and the sidebar is one long list of chats. There are no folders, so I could not group chats by project. I built a small Chrome extension, &lt;strong&gt;Chat Folders for Claude&lt;/strong&gt;, to add that. This post covers what it does and the parts that were harder than expected.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not affiliated with Anthropic. This is an independent project, not endorsed by or connected to Anthropic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc3qlxc12q32kti36otq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc3qlxc12q32kti36otq.png" alt="Chat Folders for Claude: a Folders section in the claude.ai sidebar, shown on a stand-in page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Adds a &lt;strong&gt;Folders&lt;/strong&gt; section to the claude.ai sidebar: create, rename, delete, collapse.&lt;/li&gt;
&lt;li&gt;File the open chat into a folder (a chat lives in one folder at a time). Deleting a folder never deletes a conversation.&lt;/li&gt;
&lt;li&gt;Search chat titles (not message text).&lt;/li&gt;
&lt;li&gt;Export the open chat as a Markdown file, or export a folder as a list of titles and links.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hard part: the message list is virtualised
&lt;/h2&gt;

&lt;p&gt;The Markdown export looked simple: read the messages, convert to Markdown, save a file. But the chat page only keeps a few message rows in the DOM at a time (roughly 3 to 7). If you read the page as it is, you get the part you are looking at and nothing else.&lt;/p&gt;

&lt;p&gt;Each row has a &lt;code&gt;data-index&lt;/code&gt;, so the export works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the scroll container of the message list.&lt;/li&gt;
&lt;li&gt;Jump to the top, then scroll down in steps of 0.8 of the visible height so consecutive views overlap.&lt;/li&gt;
&lt;li&gt;Wait a moment after each step for the list to draw the new rows, and collect each row once, keyed by &lt;code&gt;data-index&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sort by index, convert to Markdown, and restore the original scroll position, even if something fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If fewer messages were read than the chat has, the file says so (&lt;code&gt;(n of N messages captured)&lt;/code&gt;) instead of pretending it is complete. The site markup can change without notice, so the extension looks for structure rather than class names. If it cannot find what it needs, the feature hides itself instead of producing a broken file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and permissions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two permissions only: &lt;code&gt;storage&lt;/code&gt; and host access to &lt;code&gt;https://claude.ai/*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No network requests: no server, no analytics, no remote code. It does not call the site's internal APIs and does not read cookies.&lt;/li&gt;
&lt;li&gt;Folders are saved in &lt;code&gt;chrome.storage.local&lt;/code&gt; on your device. For each filed chat it stores the ID, the title as shown in the sidebar, and the time you added it.&lt;/li&gt;
&lt;li&gt;Message text is read only when you press an export button, only for the chat you have open, and it becomes a file your browser saves. The extension does not keep it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository has no build step and no dependencies, and there are unit tests for the storage logic, search, Markdown conversion and the scrolling collector (against a fake virtualised list).&lt;/p&gt;

&lt;p&gt;Note: the screenshot above was made on a stand-in page with invented chats, not on a real account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/chat-folders-for-claude/hmnebobeialcpbeabjkcdkmiceamndfb?utm_source=devto&amp;amp;utm_campaign=chat-folders-intro" rel="noopener noreferrer"&gt;Chat Folders for Claude&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Details: &lt;a href="https://devjar.dev/lab/chat-folders/?utm_source=devto&amp;amp;utm_campaign=chat-folders-intro" rel="noopener noreferrer"&gt;devjar.dev/lab/chat-folders&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is early (version 0.2.2). If the sidebar layout changes and something breaks, I would like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The handover package: 10 things a client should hold when your project ends</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Sun, 18 Oct 2026 21:08:05 +0000</pubDate>
      <link>https://dev.to/hssolution/the-handover-package-10-things-a-client-should-hold-when-your-project-ends-1cd1</link>
      <guid>https://dev.to/hssolution/the-handover-package-10-things-a-client-should-hold-when-your-project-ends-1cd1</guid>
      <description>&lt;p&gt;"It works on the demo server" is not the same as "the client can keep running it without you."&lt;/p&gt;

&lt;p&gt;Most handover pain we've seen doesn't come from bad code. It comes from things that were never written down or never moved out of the developer's accounts. Here is the 10-item package we put together before the last invoice, with one line on why each matters and one way to verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 items
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Repository in the client's account&lt;/strong&gt;&lt;br&gt;
Why: if the code lives only in your personal GitHub, the product becomes unfixable the day you're unreachable.&lt;br&gt;
Check: the client clones from &lt;em&gt;their&lt;/em&gt; org and sees the full commit history, not a squashed zip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Domain, cloud, app store and payment accounts in the client's name&lt;/strong&gt;&lt;br&gt;
Why: only the account owner can renew, cancel or transfer. Store publisher and merchant accounts are often hard or impossible to move later.&lt;br&gt;
Check: the client logs in to each console themselves and sees their own name, email and payout account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Admin credentials, handed over safely&lt;/strong&gt;&lt;br&gt;
Why: without admin access they can't change their own product.&lt;br&gt;
Check: share through a password manager, not chat or email. The client rotates everything immediately and disables your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A complete &lt;code&gt;.env.example&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Why: without the DB address and third-party keys nobody can rebuild the environment.&lt;br&gt;
Check: every variable name is listed with a one-line description; the real values are sent separately and bring up a fresh environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A run-and-deploy guide that works without you&lt;/strong&gt;&lt;br&gt;
Why: if the next developer's first question is "how do I deploy this?", that's paid hours.&lt;br&gt;
Check: give the doc alone to another developer on a blank machine. Can they get it running without messaging you?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Backup &lt;em&gt;and&lt;/em&gt; a tested restore&lt;/strong&gt;&lt;br&gt;
Why: a backup that has never been restored is a hope, not a backup.&lt;br&gt;
Check: say where and how often backups go, then restore the latest one into a test environment together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Paid services and monthly cost table&lt;/strong&gt;&lt;br&gt;
Why: email sending, maps, AI calls and servers tend to surface for the first time on a bill after handover.&lt;br&gt;
Check: one table with service, purpose, billing cycle and expected monthly cost, and the total roughly matches what you quoted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Licence list (open source, images, fonts)&lt;/strong&gt;&lt;br&gt;
Why: an asset that doesn't allow commercial use can mean takedowns later.&lt;br&gt;
Check: name, source and licence type per item, plus receipts for anything purchased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Warranty window and how to report bugs&lt;/strong&gt;&lt;br&gt;
Why: the first bug after launch is stressful if nobody knows where to send it.&lt;br&gt;
Check: start/end dates of the free-fix period, a reporting channel and a target reply time, in writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. A handover memo for the next developer&lt;/strong&gt;&lt;br&gt;
Why: code doesn't explain &lt;em&gt;why&lt;/em&gt; it's built this way or which parts are fragile.&lt;br&gt;
Check: at least one page on structure, known issues, postponed work and "don't touch without reading this" areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Order matters
&lt;/h2&gt;

&lt;p&gt;Go through the list &lt;em&gt;before&lt;/em&gt; the final payment. After that, everyone has less leverage to chase missing pieces. If something can't be delivered on the day, write down (email is fine) what is missing and when it will arrive.&lt;/p&gt;

&lt;p&gt;This is general practice, not legal advice. Contract wording around source code and IP is worth a professional review.&lt;/p&gt;




&lt;p&gt;The full checklist, with links to the related guides on contract clauses and acceptance testing, is here: &lt;a href="https://noligo.co.kr/en/guide/handover-checklist/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=handover-checklist-devto" rel="noopener noreferrer"&gt;Handover checklist: 10 things to receive when an outsourced project ends&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're a small dev studio in Korea (HS Solution) and we write these checklists from the client side, so we're curious what's on &lt;em&gt;your&lt;/em&gt; handover list. What's the one item you've had to chase after a project was "done"?&lt;/p&gt;

</description>
      <category>career</category>
      <category>freelance</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your zodiac animal does not change on January 1: computing Ipchun in Dart</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:37:39 +0000</pubDate>
      <link>https://dev.to/hssolution/your-zodiac-animal-does-not-change-on-january-1-computing-ipchun-in-dart-4631</link>
      <guid>https://dev.to/hssolution/your-zodiac-animal-does-not-change-on-january-1-computing-ipchun-in-dart-4631</guid>
      <description>&lt;p&gt;Ask a Korean friend their birth year and you will often get their &lt;em&gt;tti&lt;/em&gt; (띠), the zodiac animal, instead. When we built the birth-chart engine for our Korean saju app, the animal turned out to be the first thing that broke: plenty of people born in January or early February got the wrong one.&lt;/p&gt;

&lt;p&gt;The reason is that the zodiac year does not start on January 1. In saju it starts at &lt;strong&gt;Ipchun (입춘, 立春)&lt;/strong&gt;, the "start of spring" solar term. Here is how we compute it, and where the method stops being exact.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthuqjyfmlu7g3qid0jwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthuqjyfmlu7g3qid0jwl.png" alt="Ipchun moment in Korea time, 2018 to 2031" width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Feb 4" is not enough
&lt;/h2&gt;

&lt;p&gt;Most tables say Ipchun is "around February 4". That is true, but it is a &lt;em&gt;moment&lt;/em&gt;, not a date. It is the instant the Sun reaches ecliptic longitude 315°, and it drifts by about six hours a year until a leap year pulls it back.&lt;/p&gt;

&lt;p&gt;From the table above: in 2021, 2025 and 2029 it lands late on &lt;strong&gt;February 3&lt;/strong&gt; in Korea time. In 2020 and 2024 it lands in the evening of February 4. So a baby born on the afternoon of Feb 4, 2024 still belongs to the previous year's animal (Rabbit), while one born a few hours later is a Dragon.&lt;/p&gt;

&lt;p&gt;Two more traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seollal is a different rule.&lt;/strong&gt; In everyday life many Koreans switch the animal at the Lunar New Year (late January to mid-February). Saju uses Ipchun. The two rules disagree for birthdays that fall between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time zone history matters.&lt;/strong&gt; Korea used UTC+8:30 from 1954-03-21 to 1961-08-09, and had daylight saving time in the summers of 1987 and 1988. If you convert a birth time with a flat UTC+9, edge cases move.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The calculation
&lt;/h2&gt;

&lt;p&gt;We do not ship an almanac table. We compute the solar terms with the low-precision solar position from Jean Meeus, &lt;em&gt;Astronomical Algorithms&lt;/em&gt; (chapter 25), then solve for the moment the longitude hits the target with a few Newton-style steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Apparent ecliptic longitude of the Sun (degrees, 0-360)&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;sunLongitude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;2451545.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;36525&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;l0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;280.46646&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;36000.76983&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0003032&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;357.52911&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;35999.05029&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.0001537&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.914602&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.004817&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.000014&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.019993&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.000101&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.000289&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;125.04&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1934.136&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.00569&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.00478&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// Moment (UTC) when the Sun reaches [deg], searched near [approx]&lt;/span&gt;
&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="nf"&gt;termMoment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;jd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_jd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deg&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;sunLongitude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;540&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// wrap to -180..180&lt;/span&gt;
    &lt;span class="n"&gt;jd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;365.2422&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// ~1 degree per day&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fromJd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="nf"&gt;ipchun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;termMoment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;315&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;/// Saju year: before the Ipchun instant, it is still last year.&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sajuYearAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipchun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the animal is just an index: &lt;code&gt;(sajuYear - 4) % 12&lt;/code&gt; gives 0 for Rat (1984 was a Rat year). The same function with longitudes 285°, 345°, 15° and so on gives the 12 month boundaries, which saju needs for the month pillar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops being exact
&lt;/h2&gt;

&lt;p&gt;Be honest about the error bar. This formula is good to roughly 0.01° of solar longitude, which is about 15 minutes of time. Checked against published almanac times, our results land a few minutes off: for 2024 we get 17:21 KST, the commonly published Ipchun time is 17:27. We ignore ΔT (about a minute these days), which is smaller than that error.&lt;/p&gt;

&lt;p&gt;For an entertainment app that is fine for almost everyone. It is not fine for someone born within ~15 minutes of the boundary, so for those births the honest answer is "check a full ephemeris". If you need that precision, use the full VSOP87 theory or a JPL ephemeris instead of the short series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to see the result on your own birthday, our app &lt;strong&gt;Starlit Alley: Saju &amp;amp; Tarot&lt;/strong&gt; does the full four-pillar chart with this engine (Android, free to try): &lt;a href="https://play.google.com/store/apps/details?id=com.noligo.saju_app&amp;amp;utm_source=devto&amp;amp;utm_campaign=ipchun-boundary" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the plain birth-year table from 1948 to 2031 and the Seollal vs Ipchun rule in non-code form, see our guide &lt;a href="https://noligo.co.kr/en/guide/korean-zodiac-animals-by-birth-year/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ipchun-boundary" rel="noopener noreferrer"&gt;Korean Zodiac Animals by Birth Year&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Saju and zodiac readings are cultural entertainment, not predictions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>algorithms</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Before the final invoice: a 10-point acceptance checklist for outsourced software</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Fri, 16 Oct 2026 22:33:27 +0000</pubDate>
      <link>https://dev.to/hssolution/before-the-final-invoice-a-10-point-acceptance-checklist-for-outsourced-software-2fla</link>
      <guid>https://dev.to/hssolution/before-the-final-invoice-a-10-point-acceptance-checklist-for-outsourced-software-2fla</guid>
      <description>&lt;p&gt;Most outsourcing disputes I've seen don't start during development. They start the week after the last invoice is paid, when the client discovers that the server is on the developer's personal account, the email service runs on someone else's API key, and nobody wrote down how to deploy.&lt;/p&gt;

&lt;p&gt;We run a small development studio, so we sit on the delivering side of this. Over time we turned "what should be true before the final payment" into a written acceptance checklist that we send to clients &lt;em&gt;before&lt;/em&gt; delivery. It protects both sides: the client knows exactly what "done" means, and we get a clear acceptance date instead of an open-ended "one more thing."&lt;/p&gt;

&lt;p&gt;Here is the short version. If you freelance, feel free to copy it into your own contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Every feature, one row in a table.&lt;/strong&gt; Copy the features from the quote or spec into a table and mark each row &lt;em&gt;pass&lt;/em&gt; or &lt;em&gt;needs fixing&lt;/em&gt;. Clicking around the obvious screens is how missing features slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Real devices, real browsers.&lt;/strong&gt; Run the core flows on what users will actually use: an iPhone with Safari, a mid-range Android phone, Chrome on desktop. "Works on my machine" is not acceptance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The unhappy paths.&lt;/strong&gt; Submit an empty form, type a wrong password, go offline. A clear message is a pass. A blank page or a raw stack trace is a fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Accounts in the client's name.&lt;/strong&gt; Admin panel, server, database, email sending, maps, payments: each should sign in with the client's own email, not a borrowed developer account. Change passwords after handover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Source, repo, domain, server.&lt;/strong&gt; The full source lives in a repository the client owns. The domain registrant and hosting account are in the client's name. Compare against the contract line by line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deployment is written down.&lt;/strong&gt; Install, configure, build, upload, restart. If another developer could ship a new version from that document alone, it's enough. Secrets go over a separate channel, never in the README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Personal data and backups.&lt;/strong&gt; What is stored where? Are passwords hashed? Does the privacy policy match what the code actually does? Do backups run automatically, and has anyone ever &lt;em&gt;restored&lt;/em&gt; one?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. The bug-fix window is in writing.&lt;/strong&gt; How many days of free fixes, starting from which date (acceptance or delivery), and what counts as a bug versus a new feature request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Licenses for fonts, images and libraries.&lt;/strong&gt; A list of every font, photo and icon, whether commercial use is allowed, and whether credit is required. "Found it on Google Images" turns into a legal email later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Basic performance.&lt;/strong&gt; How fast the first screen appears on mobile data, whether images are oversized, whether text is readable on a phone. If the contract has numbers, test against those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tie the final payment to acceptance
&lt;/h2&gt;

&lt;p&gt;This is the part that actually prevents the arguments. Put three things in the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a review period (for example, 7 days after delivery),&lt;/li&gt;
&lt;li&gt;how fix requests are filed (in writing, against the checklist rows),&lt;/li&gt;
&lt;li&gt;and that the final payment is due a few days after the acceptance date, which is the day every row reads &lt;em&gt;pass&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The filled-in table becomes the acceptance record. No one has to remember what was agreed in a call.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is general information, not legal advice. Have important contracts reviewed by a professional.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Full version
&lt;/h2&gt;

&lt;p&gt;The longer guide has an example for each check and sample contract wording: &lt;a href="https://noligo.co.kr/en/guide/acceptance-checklist/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=acceptance-checklist-devto" rel="noopener noreferrer"&gt;Acceptance Checklist: 10 Things to Verify Before the Final Payment&lt;/a&gt;. It's on our studio site (HS Solution), so yes, that's our page.&lt;/p&gt;

&lt;p&gt;What's on your own handover list that isn't here? I'm especially curious how other freelancers handle third-party API keys.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was written by an AI assistant from the checklist we use in our own projects.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>productivity</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One modulo gives you a Korean Saju day pillar (and Seoul's 1900 time zone breaks it)</title>
      <dc:creator>HS Solution</dc:creator>
      <pubDate>Fri, 16 Oct 2026 16:35:20 +0000</pubDate>
      <link>https://dev.to/hssolution/one-modulo-gives-you-a-korean-saju-day-pillar-and-seouls-1900-time-zone-breaks-it-3c0n</link>
      <guid>https://dev.to/hssolution/one-modulo-gives-you-a-korean-saju-day-pillar-and-seouls-1900-time-zone-breaks-it-3c0n</guid>
      <description>&lt;p&gt;In Korean Saju (the "Four Pillars" of birth), the &lt;strong&gt;day pillar&lt;/strong&gt; is the one part of the chart that is pure arithmetic. No solar terms, no lunar calendar, no lookup tables — just a 60-day cycle that has been running without a break for a very long time.&lt;/p&gt;

&lt;p&gt;So it should be a one-liner. It mostly is. But the obvious JavaScript version gives the wrong answer for &lt;strong&gt;every date&lt;/strong&gt; if your machine is set to Korean time. Here is the math, the bug, and the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-day cycle
&lt;/h2&gt;

&lt;p&gt;Each day gets a pair: one of 10 Heavenly Stems and one of 12 Earthly Branches. Both advance by one every day, so the pair repeats every lcm(10, 12) = 60 days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STEMS&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;甲乙丙丁戊己庚辛壬癸&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Gap, Eul, Byeong, Jeong, Mu, Gi, Gyeong, Sin, Im, Gye&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BRANCHES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;子丑寅卯辰巳午未申酉戌亥&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Ja, Chuk, In, Myo, Jin, Sa, O, Mi, Sin, Yu, Sul, Hae&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because both counters move together, a single index &lt;code&gt;i&lt;/code&gt; in &lt;code&gt;0..59&lt;/code&gt; is enough: the stem is &lt;code&gt;i % 10&lt;/code&gt; and the branch is &lt;code&gt;i % 12&lt;/code&gt;. Index 0 is 甲子 (Gap-ja), index 59 is 癸亥 (Gye-hae).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick an anchor, count days
&lt;/h2&gt;

&lt;p&gt;All you need is one date whose pillar you know. 31 January 1900 was 甲辰 (Gap-jin), index 40. From there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dayPillar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// works for dates before 1900 too&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;STEMS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;BRANCHES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dayPillar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "戊午"  (Mu-o, 55th day)&lt;/span&gt;
&lt;span class="nf"&gt;dayPillar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "己未"  (Gi-mi)&lt;/span&gt;
&lt;span class="nf"&gt;dayPillar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1949&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "甲子"  (start of a cycle)&lt;/span&gt;
&lt;span class="nf"&gt;dayPillar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "甲子" — also a cycle start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;((x % 60) + 60) % 60&lt;/code&gt; — JavaScript's &lt;code&gt;%&lt;/code&gt; keeps the sign, so dates before the anchor would give a negative index without it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Date.UTC&lt;/code&gt; and &lt;code&gt;Math.round&lt;/code&gt; — this is the part people skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug: local time + &lt;code&gt;Math.floor&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The "obvious" version uses local dates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran both versions for 15 May 1990 under different &lt;code&gt;TZ&lt;/code&gt; settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TZ&lt;/th&gt;
&lt;th&gt;local-date version&lt;/th&gt;
&lt;th&gt;UTC version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UTC&lt;/td&gt;
&lt;td&gt;庚辰&lt;/td&gt;
&lt;td&gt;庚辰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;America/New_York&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;己卯&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;庚辰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Europe/London&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;己卯&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;庚辰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asia/Seoul&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;己卯&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;庚辰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;New York and London are wrong because of daylight saving: 15 May is in summer time, 31 January is not, so the gap is one hour short of a whole number of days, and &lt;code&gt;Math.floor&lt;/code&gt; drops a day. In January the same code happens to work, which is why this bug survives testing.&lt;/p&gt;

&lt;p&gt;Seoul is worse. Daylight saving isn't the problem there. In the time zone database, Seoul in 1900 was on local mean time, &lt;strong&gt;UTC+8:27:52&lt;/strong&gt;, not today's +9:00. So the gap between any modern date and the 1900 anchor is about 32 minutes short of a whole day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Asia&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;Seoul  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;864&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mf"&gt;32975.9777&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Math.floor&lt;/code&gt; turns that into 32975, and &lt;strong&gt;every&lt;/strong&gt; date comes out one pillar early. The machine most likely to run a Korean astrology calculator is the one where the naive code always fails.&lt;/p&gt;

&lt;p&gt;The fix is boring: do calendar math on calendar dates. &lt;code&gt;Date.UTC&lt;/code&gt; has no time zone and no DST, and &lt;code&gt;Math.round&lt;/code&gt; absorbs any leftover floating-point dust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the day pillar does not handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Year and month pillars&lt;/strong&gt; are a different problem. They change at solar terms (the year at Ipchun, around 4 February), not on 1 January, so you need an astronomical table or a good library for those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 23:00 boundary.&lt;/strong&gt; Some schools start the new day at 23:00 (the start of the 子 double hour), others at midnight. My calculator uses midnight on the calendar date and says so on the page. Whichever you pick, document it, because users will compare results across apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Birth time zone.&lt;/strong&gt; Use the calendar date where the person was born. Don't use the date converted to the server's zone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I put this exact function behind a small free page that shows your day pillar and what the Day Master element traditionally means: &lt;a href="https://noligo.co.kr/en/guide/day-master-calculator/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=daypillar-devto" rel="noopener noreferrer"&gt;Find Your Day Master&lt;/a&gt;. It is entertainment and cultural reference, not prediction.&lt;/p&gt;

&lt;p&gt;The same rule runs inside our Android app Starlit Alley, which builds the full chart (&lt;a href="https://play.google.com/store/apps/details?id=com.noligo.saju_app&amp;amp;referrer=utm_source%3Ddevto%26utm_medium%3Darticle%26utm_campaign%3Ddaypillar-devto" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;). I'm part of the small team that makes it.&lt;/p&gt;

&lt;p&gt;If you've hit the Seoul LMT offset in other date code, I'd like to hear where it showed up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was drafted with AI assistance. Every result in it, including the table, came from running the code above in Node.js.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>algorithms</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
