<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ruri Build Notes]]></title><description><![CDATA[Ruri Build Notes]]></description><link>https://sungprofile-dev.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 22:03:42 GMT</lastBuildDate><atom:link href="https://sungprofile-dev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Expert talk vs what the market did: plotting named forecasts on real price charts]]></title><description><![CDATA[News aggregators answer "what happened". Readers usually want the next question answered: so what changes for me? I added two features to Ruri Jupjup, a…]]></description><link>https://sungprofile-dev.hashnode.dev/expert-talk-vs-what-the-market-did-plotting-named-forecasts-on-real-price-charts</link><guid isPermaLink="true">https://sungprofile-dev.hashnode.dev/expert-talk-vs-what-the-market-did-plotting-named-forecasts-on-real-price-charts</guid><dc:creator><![CDATA[Ruri Jupjup (루리의 줍줍)]]></dc:creator><pubDate>Mon, 19 Oct 2026 22:37:43 GMT</pubDate><content:encoded><![CDATA[<p>News aggregators answer "what happened". Readers usually want the next question answered: <strong>so what changes for me?</strong> I added two features to <a href="https://sungprofile.cloud/money/experts/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">Ruri Jupjup</a>, a free Korean information site I build with an AI coding agent:</p>
<ol>
<li>an <strong>"impact on my life" card</strong> under news issues, and</li>
<li>a <strong>"what experts said vs what the market did"</strong> page that plots named expert statements as dots on real price charts and checks them 7 and 30 days later.</li>
</ol>
<p>The constraint was strict: no invented sentences, no invented numbers, and no server. Here is how it is built.</p>
<p><img src="https://ruribuildnotes.wordpress.com/wp-content/uploads/2026/10/shot_experts.png" alt="Expert statements on a 30-day price chart, with impact card" /></p>
<h2>1. A rule table instead of generated text</h2>
<p>If a model writes "this could raise your loan interest" on the fly, it will eventually write something plausible and wrong. So impact sentences come only from a hand-written JSON rule table.</p>
<ul>
<li><strong>10 rules</strong>: interest rates, FX, oil, housing, semiconductors, stocks, crypto, prices, policy, security</li>
<li>each rule = keyword regex + 1–3 lines of <em>"who is affected → what may change → where to check (link)"</em> + a small gauge (my wallet / daily life / schedule: low, mid, high) + which market series to attach</li>
<li>wording is always conditional ("may", "can"), and every card ends with <em>general information, depends on your situation, not investment advice</em></li>
<li>issues containing death/accident terms never get a card (1 of 60 today)</li>
</ul>
<p>Result on today's build: <strong>7 of 60</strong> news issues and <strong>7 of 26</strong> trending topics matched a rule and got a card. Most issues do not affect a household budget, and the card simply does not appear for them.</p>
<p>Numbers in a card come only from the article headline (a regex for %, %p, won, dollars, bp and dates), printed next to the outlet and title so there is never a number without a source.</p>
<h2>2. Public price data, checked against robots.txt</h2>
<table>
<thead>
<tr>
<th>Series</th>
<th>Source</th>
<th>Days</th>
</tr>
</thead>
<tbody><tr>
<td>S&amp;P 500, Nasdaq Composite, KRW/USD, WTI, US 10-year</td>
<td>FRED public CSV (<code>fredgraph.csv?id=…</code>), allowed for <code>User-agent: *</code></td>
<td>81–83</td>
</tr>
<tr>
<td>Bitcoin (KRW)</td>
<td>Upbit public API, daily candles</td>
<td>120</td>
</tr>
<tr>
<td>KOSPI, Korean bonds, a chip index</td>
<td>no free source that allows crawling yet → statements only</td>
<td>—</td>
</tr>
</tbody></table>
<p>Six series download in <strong>1.6 s</strong> into a <strong>12.8 KB</strong> JSON file, <strong>once a day</strong>. The hourly site build only reads that cache. If one series fails, it keeps yesterday's values for that series only.</p>
<h2>3. Checking statements after the fact</h2>
<ul>
<li>value on the statement date = that day's close, or the last trading day before it</li>
<li>value 7 days later = first trading day on or after date + 7; until then the page says <em>"check on 10/27"</em></li>
<li>an optimistic statement followed by a rise (or a cautious one followed by a fall) is shown as <em>"moved as stated"</em>, otherwise <em>"moved the other way"</em>: facts only, no mocking</li>
<li>a per-institution leaderboard appears only after <strong>5 judged statements</strong>; below that the page shows "sample N"</li>
</ul>
<p>Right now there are <strong>15 named expert statements in 30 days and 0 judged</strong>, because collection started on 10/16 and no 7-day window has closed yet. The rule "don't rank before there is data" is visible on the page instead of hidden in code.</p>
<h2>4. Static JSON + one SVG path</h2>
<p>There is no charting library. At build time each chart is a single SVG <code>path</code> plus <code>circle</code>s for statements; the only script opens the headline and source link when a dot is tapped. FRED lags a day or two, so statements newer than the last price point used to pile up at the right edge. The fix: extend the axis to today and draw dots right of a dashed "no data yet" line in a lighter colour.</p>
<p>Size of the whole thing: impact code <strong>62 lines</strong> + rule JSON <strong>44 lines</strong>, market module <strong>86 lines</strong>. A full site build takes about <strong>89 s</strong> and the site is plain static HTML on GitHub Pages.</p>
<h2>5. Opinion lives in a labelled box</h2>
<p>The same data feeds a weekly commentary piece, for example <a href="https://sungprofile.cloud/review/ai-chip-rebound-1020/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">why "AI bubble" talk came back the day the Nasdaq rose 1.1%</a>. It keeps facts (with links), the conflicting expert quotes, the actual index moves and the impact card separate from opinion, which sits in its own clearly labelled box.</p>
<p><img src="https://ruribuildnotes.wordpress.com/wp-content/uploads/2026/10/shot_review.png" alt="Commentary page: facts, conflicting statements, real index moves" /></p>
<h2>Try it</h2>
<ul>
<li>Expert talk vs market: <a href="https://sungprofile.cloud/money/experts/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">sungprofile.cloud/money/experts</a></li>
<li>Full build notes (Korean): <a href="https://sungprofile.cloud/dev/issue-impact-market-pipeline/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">sungprofile.cloud/dev/issue-impact-market-pipeline</a></li>
<li>The rest of the site (contests, support programs, 1,350 fridge recipes): <a href="https://sungprofile.cloud/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">sungprofile.cloud</a></li>
</ul>
<p><em>Built with an AI coding assistant; all numbers re-measured on the 2026-10-20 build. General information, not investment advice.</em></p>
]]></content:encoded></item><item><title><![CDATA[WMIC is gone on Windows 11: why my nightly job silently did nothing, and what replaced it]]></title><description><![CDATA[I have a nightly job on a Windows desktop: at 00:00 it collects data, rebuilds a static site, pushes it, and packages a summary for another machine. Before…]]></description><link>https://sungprofile-dev.hashnode.dev/wmic-is-gone-on-windows-11-why-my-nightly-job-silently-did-nothing-and-what-replaced-it</link><guid isPermaLink="true">https://sungprofile-dev.hashnode.dev/wmic-is-gone-on-windows-11-why-my-nightly-job-silently-did-nothing-and-what-replaced-it</guid><dc:creator><![CDATA[Ruri Jupjup (루리의 줍줍)]]></dc:creator><pubDate>Mon, 19 Oct 2026 15:57:44 GMT</pubDate><content:encoded><![CDATA[<p>I have a nightly job on a Windows desktop: at 00:00 it collects data, rebuilds a static site, pushes it, and packages a summary for another machine. Before starting each heavy step it checks whether the same script is already running (an hourly build sometimes overlaps), and waits if so.</p>
<p>Last night every step failed in the first second with:</p>
<pre><code class="language-plaintext">FileNotFoundError(2, 'The system cannot find the file specified')
</code></pre>
<p>Nothing ran, and the only thing that "worked" was the final packaging step, which happily summarised yesterday's data. Here is what happened and the replacement I use now.</p>
<h2>The cause: <code>wmic</code> is gone</h2>
<p>The "is it running?" check shelled out to <code>wmic</code>:</p>
<pre><code class="language-python">r = subprocess.run(
    ["wmic", "process", "where", "name like 'python%'", "get", "commandline"],
    capture_output=True, text=True)
return script_name in r.stdout
</code></pre>
<p>WMIC has been deprecated for years, and on current Windows 11 builds it is not installed by default (it became an optional feature, and newer installs simply do not have it). <code>subprocess.run</code> therefore cannot find the executable and raises <code>FileNotFoundError</code> before anything else happens.</p>
<p>Because I wrapped each step in <code>try/except</code> and only logged the error, the job did not crash. It just quietly skipped the work. That is the real lesson: a guard that fails should not be treated the same as "nothing to wait for".</p>
<h2>Replacement 1: PowerShell CIM (no extra packages)</h2>
<p><code>Get-CimInstance Win32_Process</code> returns the same data WMIC used to:</p>
<pre><code class="language-python">import subprocess

NO_WINDOW = 0x08000000  # don't flash a console window from a scheduled task

def is_running(script_name: str) -&gt; bool:
    ps = ("(Get-CimInstance Win32_Process "
          "-Filter \"Name like 'python%'\").CommandLine")
    r = subprocess.run(["powershell", "-NoProfile", "-Command", ps],
                       capture_output=True, text=True, errors="ignore",
                       creationflags=NO_WINDOW)
    if r.returncode != 0:
        raise RuntimeError(f"process check failed: {r.stderr[-200:]}")
    return script_name in r.stdout
</code></pre>
<p>Two details matter here:</p>
<ul>
<li><p>The filter uses WQL, so it is <code>Name like 'python%'</code> (with <code>%</code>, not <code>*</code>), the same syntax WMIC used.</p>
</li>
<li><p><code>CREATE_NO_WINDOW</code> keeps a scheduled task from popping up a PowerShell window on the desktop.</p>
</li>
</ul>
<p>PowerShell start-up costs a few hundred milliseconds, which is irrelevant for a once-a-night check.</p>
<h2>Replacement 2: <code>psutil</code> (if you can add a dependency)</h2>
<pre><code class="language-python">import psutil

def is_running(script_name: str) -&gt; bool:
    for p in psutil.process_iter(["name", "cmdline"]):
        name = (p.info["name"] or "").lower()
        if name.startswith("python") and any(script_name in a for a in (p.info["cmdline"] or [])):
            return True
    return False
</code></pre>
<p>This is faster and cross-platform. Some processes deny access to <code>cmdline</code>; <code>process_iter</code> with the attribute list returns <code>None</code> for those instead of raising, which is why the <code>or []</code> is there.</p>
<h2>Make the guard fail loudly</h2>
<p>The bigger fix was in the caller. Before:</p>
<pre><code class="language-python">try:
    wait_until_free("build.py")
    run_build()
except Exception as e:
    log("build failed", e)
</code></pre>
<p>After, the wait has a timeout and a broken check stops the run instead of being mistaken for "free":</p>
<pre><code class="language-python">for _ in range(60):              # wait up to 30 minutes
    if not is_running("build.py"):
        break
    time.sleep(30)
else:
    raise RuntimeError("build.py still running after 30 min")
run_build()
</code></pre>
<p>and the summary step now refuses to package if the build step did not succeed, so a failed night can't look like a quiet one.</p>
<h2>Checklist if you have old Windows automation</h2>
<ul>
<li><p><code>grep -r wmic</code> your scripts and scheduled tasks.</p>
</li>
<li><p>Replace <code>wmic process</code> with <code>Get-CimInstance Win32_Process</code> or <code>psutil</code>.</p>
</li>
<li><p>Replace <code>wmic os get ...</code> / <code>wmic logicaldisk ...</code> the same way (<code>Win32_OperatingSystem</code>, <code>Win32_LogicalDisk</code>).</p>
</li>
<li><p>Make sure a failing pre-check stops the job instead of being swallowed.</p>
</li>
</ul>
<hr />
<p><em>Written with the help of an AI coding assistant, from a bug in my own scheduled job.</em></p>
<h2>Where this runs</h2>
<p>This nightly job keeps <strong><a href="https://sungprofile.cloud/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev_2610">Ruri Jupjup</a></strong> fresh: a free Korean site I build with an AI coding agent that lists open contests and government support programs (rebuilt every hour) and has an ingredient-based finder over ~1,350 home recipes. The 00:00 run collects new listings, rebuilds and pushes the static site, and packages a summary for my other machine. That is why a silent failure mattered: the site simply stopped updating for a night.</p>
]]></content:encoded></item><item><title><![CDATA[Collecting data the polite way: robots.txt first, then RSS and public APIs, and dates you can trust]]></title><description><![CDATA[My site is a pile of Python scripts that run every hour: collect, process, build static HTML, git push. The collecting part is where I made most of my…]]></description><link>https://sungprofile-dev.hashnode.dev/collecting-data-the-polite-way-robots-txt-first-then-rss-and-public-apis-and-dates-you-can-trust</link><guid isPermaLink="true">https://sungprofile-dev.hashnode.dev/collecting-data-the-polite-way-robots-txt-first-then-rss-and-public-apis-and-dates-you-can-trust</guid><dc:creator><![CDATA[Ruri Jupjup (루리의 줍줍)]]></dc:creator><pubDate>Sun, 18 Oct 2026 22:20:34 GMT</pubDate><content:encoded><![CDATA[<p>My site is a pile of Python scripts that run every hour: collect, process, build static HTML, <code>git push</code>. The collecting part is where I made most of my early mistakes, so this post is about that: what I decided to collect from, what I refused to collect from, and how I stopped bad dates from sneaking into the output.</p>
<h2>1. Decide the source list by reading robots.txt, not by what's convenient</h2>
<p>For news, the useful thing is that most publishers expose an RSS feed and their <code>robots.txt</code> doesn't forbid it. For stock and crypto commentary I wanted the big portals, and that is where I hit a wall: several of them disallow automated collection, so they are simply not in the list. In the module docstring:</p>
<pre><code class="language-python">"""Source: only securities/crypto RSS from outlets that allow collection
(some large finance portals, a news aggregator and a global finance site
disallow it in robots.txt, so they are not used). Coin prices come from
the exchange's public API."""
</code></pre>
<p>The rule I use is boring and I keep it boring: before adding a source, fetch <code>/robots.txt</code> and read it. If the path I need is disallowed, I don't use that source, even if I could technically scrape it. A quick check looks like this:</p>
<pre><code class="language-python">from urllib.robotparser import RobotFileParser

def allowed(url, ua='*'):
    base = '/'.join(url.split('/')[:3])
    rp = RobotFileParser(base + '/robots.txt')
    rp.read()
    return rp.can_fetch(ua, url)
</code></pre>
<p>That helper is a sketch for the check I do before adding a feed. The point is the habit, not the code.</p>
<p>For the data that isn't news, the same idea applies: public trend RSS, a public ticker endpoint, and public-sector open data. One public recipe database I experimented with has fields named <code>MANUAL01</code> to <code>MANUAL20</code> for the cooking steps, and the step text can end with a stray a/b/c marker. A one-line <code>re.sub(r'[a-z]$', '', step)</code> cleans it. Public data tends to be accurate and badly formatted, so budget time for formatting.</p>
<h2>2. Parse RSS without a library, but handle the two things that break</h2>
<p>I parse feeds with a few regexes instead of a feed library. Two things bit me:</p>
<p><strong>Encoding.</strong> Some Korean outlets still serve <code>euc-kr</code> / <code>cp949</code>. <code>requests</code> guesses wrong often enough to produce mojibake. I sniff the XML declaration instead:</p>
<pre><code class="language-python">def decode(r):
    head = r.content[:300].decode('ascii', 'ignore').lower()
    for enc in ('euc-kr', 'cp949', 'ks_c_5601'):
        if enc in head:
            return r.content.decode('cp949', 'ignore')
    return r.content.decode('utf-8', 'ignore')
</code></pre>
<p><strong>Missing dates.</strong> In the news collector, when an item has no parseable <code>pubDate</code>, the fallback is "now":</p>
<pre><code class="language-python">try:
    ts = email.utils.parsedate_to_datetime(ds).timestamp() ...
except Exception:
    ts = time.time()
</code></pre>
<p>For news that is filtered to the last 36 hours, that's tolerable. For anything where age matters it's a trap, and I fell into it: an undated, year-old article in a feed was shown as "today's report".</p>
<h2>3. Dates you can trust: RSS, then page metadata, then URL, then give up</h2>
<p>For the analyst-comment collector, an item without a reliable date is dropped. The idea: use the RSS date if there is one, otherwise the date from the page's own metadata, otherwise a date found in the URL, and otherwise nothing. Here is the URL part and the sanity check, trimmed:</p>
<pre><code class="language-python">def url_date(u):
    m = re.search(r'(20[12]\d)[-/]?(0[1-9]|1[0-2])[-/]?(0[1-9]|[12]\d|3[01])', u)
    if m:
        return datetime.datetime(int(m[1]), int(m[2]), int(m[3]), 12, tzinfo=KST).timestamp()

# RSS date -&gt; page metadata (filled in earlier) -&gt; date inside the URL. Unknown: drop.
if a['ts'] is None:
    a['ts'] = int(ud) if ud else None
elif ud and a['ts'] - ud &gt; 3 * 86400:
    a['ts'] = int(ud)            # URL says it's much older than the feed claims
items = [a for a in items if a['ts'] and now - a['ts'] &lt; 7 * 86400]
</code></pre>
<p>Two details I like: the URL date wins when it is more than 3 days older than the RSS date (re-published old articles are exactly what the RSS date lies about), and "unknown" means "drop", never "now".</p>
<h2>What I'd tell myself at the start</h2>
<ul>
<li>Write the allowed-source list down in the code, with the reason for each exclusion.</li>
<li>A missing date is a missing fact. Don't fill it with <code>time.time()</code> unless a wrong date is harmless.</li>
<li>Public data is free to use, but the formatting work isn't free.</li>
</ul>
<p>The site this runs on is Korean; the build notes (in Korean) are here: <a href="https://sungprofile.cloud/ailog/?src=hashnode">https://sungprofile.cloud/ailog/?src=hashnode</a></p>
]]></content:encoded></item><item><title><![CDATA[I let an AI coding agent build a 1,360-page site with a $0 server bill]]></title><description><![CDATA[I'm not a full-time developer. Over two days and 110 commits I've been running an experiment: I describe what I want in plain Korean, and an AI coding…]]></description><link>https://sungprofile-dev.hashnode.dev/i-let-an-ai-coding-agent-build-a-1-300-page-site-with-a-0-server-bill</link><guid isPermaLink="true">https://sungprofile-dev.hashnode.dev/i-let-an-ai-coding-agent-build-a-1-300-page-site-with-a-0-server-bill</guid><category><![CDATA[AI]]></category><category><![CDATA[claude]]></category><category><![CDATA[serverless]]></category><category><![CDATA[webdev]]></category><category><![CDATA[side project]]></category><dc:creator><![CDATA[Ruri Jupjup (루리의 줍줍)]]></dc:creator><pubDate>Sun, 18 Oct 2026 09:47:49 GMT</pubDate><content:encoded><![CDATA[<p>I'm not a full-time developer. Over two days and 110 commits I've been running an experiment: I describe what I want in plain Korean, and an AI coding agent (Claude Code) does the planning, coding, deployment, debugging, images and short videos. The result is a Korean information site with 1,360 static pages that rebuilds itself every hour — and the server/API bill is $0 (domain and the AI subscription are separate).</p>
<h2>The $0 constraint</h2>
<ul>
<li>Hosting: GitHub Pages (static HTML) — free</li>
<li>Login, bookmarks, visit counter, web-push subscriptions: Cloudflare Workers + D1 free tier</li>
<li>Photo-to-ingredients recognition for a "what can I cook with my fridge" tool: a public vision model on Workers AI, capped per day to stay inside the free allocation</li>
<li>Data: publishers' public RSS feeds (only where robots.txt allows), Upbit's public crypto API, Google Trends RSS</li>
<li>Character art: open models on my own GPU</li>
<li>Automation: Windows Task Scheduler on my PC, every hour: collect → process → build → git push</li>
</ul>
<h2>Things I found interesting</h2>
<p><strong>Grouping the same story across 15 news outlets.</strong> The first version linked articles in a chain (A≈B, B≈C), which merged completely different stories A and C. Switching to "compare everything against a seed article" fixed it.</p>
<p><strong>A "verification signals" score, not a fact-check verdict.</strong> Each article gets points for cross-reporting, named (vs anonymous or social-media) sources, whether numbers and double-quoted quotes in the headline actually appear in the body, and a flag when outlets report different casualty numbers for the same event. The page always says this is a reading aid, not a final judgement.</p>
<p><strong>Analyst reports as nodes.</strong> One brokerage-report article = one node (firm, direction, old → new price target, reason tags). Nodes roll up into per-stock "reasons up / reasons down" trees.</p>
<h2>Where the AI got it wrong</h2>
<ul>
<li>A year-old article without a date slipped into an RSS feed and showed up as "today's report". Fix: resolve dates from RSS → URL → page metadata, drop anything older than 7 days, and flag implausible price-target jumps.</li>
<li>Single quotes used for emphasis in Korean headlines were treated as quotations and penalised. Fix: only compare double-quoted text.</li>
<li>Generated characters with odd hands. Fix: only hand-checked images are used.</li>
</ul>
<p>The full cost table, architecture and the actual instructions I gave are on the build log page (in Korean): <a href="https://sungprofile.cloud/ailog/?src=hashnode">https://sungprofile.cloud/ailog/?src=hashnode</a></p>
<p><strong>Update (Oct 18, evening)</strong></p>
<ul>
<li>The home page is now four themed entrances; news, money, contests, everyday-life and the AI build log each became a separate screen with its own colours and menu. The entrance matching where you came from (?src=) is shown first.</li>
<li>Per-theme chatbots that run entirely in the browser (no model): today's issues and questionable articles; crypto prices and price-target upgrades/downgrades; subsidy search and housing-lottery schedules. Data loads only when the panel is opened.</li>
<li>Housing-lottery helper: enter your situation → score (out of 84) + supply types worth trying + matching notices open right now.</li>
<li>Money section: an investment-policy table checked against official sources, general age-based principles, and on-the-record expert comments grouped by topic, side by side when they disagree (not investment advice).</li>
<li>Search clean-up: RSS feed, sitemap lastmod that only changes when a content hash changes, noindex on thin auto-generated pages, and pre-rendering JS-drawn screens to static HTML (text visible to crawlers went from 687 to 13,605 characters).</li>
<li>Analytics: channels grouped by first touch; automated health-check traffic from overseas servers excluded (12 hits had slipped in on day one).</li>
<li>Updated numbers: 2 days · 110 commits · 1,360 pages (about 2,400 old multilingual pages removed). Server/API cost is still $0.</li>
</ul>
]]></content:encoded></item></channel></rss>