<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>I Spent the Past Few Weeks Building a Cinema App with React &amp; Firebase 🎬</title>
      <dc:creator>Pam De Ramos</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:28:53 +0000</pubDate>
      <link>https://dev.to/japam28/i-spent-the-past-few-weeks-building-a-cinema-app-with-react-firebase-5f0l</link>
      <guid>https://dev.to/japam28/i-spent-the-past-few-weeks-building-a-cinema-app-with-react-firebase-5f0l</guid>
      <description>&lt;p&gt;Like many developers, I started this project with a simple idea:&lt;/p&gt;

&lt;p&gt;"What if there was a place that made discovering films feel intentional again?"&lt;/p&gt;

&lt;p&gt;That idea eventually became Cineaste.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://cineastefilm.org/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;cineastefilm.org&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Instead of trying to compete with streaming services, I wanted to build something for people who genuinely love cinema—whether that's discovering forgotten classics, keeping track of what they've watched, or simply finding their next great film.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I decided to keep the project relatively simple:&lt;/p&gt;

&lt;p&gt;React&lt;br&gt;
Firebase Authentication&lt;br&gt;
Cloud Firestore&lt;br&gt;
Firebase Hosting&lt;/p&gt;

&lt;p&gt;For Premium memberships, I'm using Gumroad, and I'm currently preparing the site for Google AdSense for free users.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://pamelarose85.gumroad.com/l/cnxclr" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2F8f1wcf79najqo7lw8ve7vl34astq" height="461" class="m-0" width="1005"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://pamelarose85.gumroad.com/l/cnxclr" rel="noopener noreferrer" class="c-link"&gt;
            Cineaste Lifetime Access
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            🎬 Welcome to CineasteYour purchase includes lifetime access to the Cineaste cinematic discovery platform.ACCESS YOUR THE WEBSITE/APP:https://cineastefilm.orgHOW TO ACTIVATE: Free to Log-in, More Categories for Premium Users Premium access will be activated automatically or shortly after purchase (Please allow 30 minutes-8 hours for your account to be activated. It'll be worth it. Thank you ) Features include: After-Hours Lounge - listen to curated radio stations, play mini games, news and your own cinema journal Late-Night Thought - quotes to inspire your film discovery Letterboxd list importing and direct link to the Letterboxd page per movie 'Refresh List' button, will give new list of movies, only clickable after watching the whole list currently showed on that category Short plot per film so basically no need to check another website, and search for the same movie plus RT, and awards integration Direct link to listed movie's soundtrack on Spotify Direct link to each movie’s Wikipedia page to complete immersion Minimal/no clutter design philosophy *Find the right film without endless scrolling*How to add Cineaste to your phone:iPhone: Open Cineaste in Safari, tap the Share button, scroll down, tap Add to Home Screen, then tap Add.Android: Open Cineaste in Chrome, tap the three-dot menu, tap Add to Home screen or Install app, then tap Add.Thank you for supporting independent creative software.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.gumroad.com%2Fimages%2Fpink-icon.png" width="81" height="81"&gt;
          pamelarose85.gumroad.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some of the features I've built include:&lt;/p&gt;

&lt;p&gt;🎞️ Curated film collections&lt;br&gt;
✅ Personal watch tracker&lt;br&gt;
📚 Custom Letterboxd list importer&lt;br&gt;
🌙 Late-Night Thoughts journal&lt;br&gt;
🎲 Cineaste Sweep mini-game&lt;br&gt;
🍿 Tonight's Pick&lt;br&gt;
🎭 Personal Cinema Profile&lt;br&gt;
⭐ Premium-only collections and features&lt;/p&gt;

&lt;p&gt;One of the biggest changes I made recently was moving nearly all user data into Firestore so every signed-in user has their own unique experience.&lt;/p&gt;

&lt;p&gt;Things like watched films, Cinema Profile, Tonight's Pick, and game progress are now synced to each individual account instead of living only in local storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things I Didn't Expect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building authentication ended up taking far longer than I expected.&lt;/p&gt;

&lt;p&gt;Adding email verification, syncing user-specific data, handling Premium access, and making everything work seamlessly across devices turned out to be much more involved than simply calling a Firebase function.&lt;/p&gt;

&lt;p&gt;I also discovered that a lot of the work happens outside of the "fun" features—writing a Privacy Policy, Terms of Use, About page, responsive layouts, and all the small details that make an app feel complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few things this project has reinforced:&lt;/p&gt;

&lt;p&gt;Small improvements every day compound quickly.&lt;br&gt;
Firebase is fantastic for solo developers.&lt;br&gt;
UI polish takes longer than expected—but it's worth it.&lt;br&gt;
Building something people actually use is very different from building tutorials.&lt;br&gt;
What's Next&lt;/p&gt;

&lt;p&gt;Now I'm focusing on:&lt;/p&gt;

&lt;p&gt;Google AdSense approval&lt;br&gt;
Final UI polish&lt;br&gt;
Performance improvements&lt;/p&gt;

&lt;p&gt;Please try it and upgrade to premium to support. :P &lt;br&gt;
Thanks for reading! 🎬&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>react</category>
      <category>firebase</category>
    </item>
    <item>
      <title>How Zero-Knowledge Encryption Actually Works (with the Web Crypto API)</title>
      <dc:creator>Sudip Bhandari</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:21:53 +0000</pubDate>
      <link>https://dev.to/sudeepbhandari/how-zero-knowledge-encryption-actually-works-with-the-web-crypto-api-1f53</link>
      <guid>https://dev.to/sudeepbhandari/how-zero-knowledge-encryption-actually-works-with-the-web-crypto-api-1f53</guid>
      <description>&lt;p&gt;&lt;strong&gt;"Zero-knowledge"&lt;/strong&gt; gets thrown around a lot in privacy marketing, but it has a precise, testable meaning: the server operator has no ability to read your data — not as a policy promise, but as a mathematical fact. Let's build the mental model from primitives, using nothing but the browser's native window.crypto.subtle (the Web Crypto API). No third-party crypto libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The threat model
&lt;/h2&gt;

&lt;p&gt;Assume the server is hostile, or will be subpoenaed, or will be breached. A note-taking app that "encrypts at rest" and promises not to peek doesn't survive this model — the operator holds the keys. Zero-knowledge flips it: the key never exists on the server in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Deriving a key from a password (PBKDF2)
&lt;/h2&gt;

&lt;p&gt;Passwords are low-entropy, so you never use them directly as keys. You stretch them with a slow KDF. A production-safe configuration in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Algorithm: PBKDF2 with SHA-256&lt;/li&gt;
&lt;li&gt;Iterations: 100,000 (raises brute-force cost)&lt;/li&gt;
&lt;li&gt;Salt: 128-bit random per note (defeats rainbow tables)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&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;enc&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;TextEncoder&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;keyMaterial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PBKDF2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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;deriveKey&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PBKDF2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;keyMaterial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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;encrypt&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;decrypt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;h2&gt;
  
  
  3. Encrypting with AES-256-GCM
&lt;/h2&gt;

&lt;p&gt;GCM is authenticated encryption: it gives you confidentiality and tamper-detection in one primitive. Generate a fresh 96-bit IV for every encryption so identical plaintext never yields identical ciphertext.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&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;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 128-bit&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 96-bit&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;key&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&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 these three values leave the browser:&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&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 critical line is the last comment. The server receives salt, iv, and ciphertext — three values that are indistinguishable from random bytes without the password. There is no plaintext to leak, subpoena, or sell.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why this is the only honest form of "private notes"
&lt;/h2&gt;

&lt;p&gt;If a service can read your notes, then "we don't" is a policy that can change with an acquisition, a court order, or a breach. If a service architecturally cannot read your notes, the guarantee holds even when the company doesn't. That's the whole point of a &lt;a href="https://www.securetext.cloud/how-it-works" rel="noopener noreferrer"&gt;zero-knowledge architecture&lt;/a&gt;: compromise of the server yields ciphertext, not content.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Persistent vs. ephemeral
&lt;/h2&gt;

&lt;p&gt;There are two families here. Ephemeral tools (think one-time secret links) destroy the note after a single read — great for handing someone a password once. Persistent zero-knowledge notepads keep the encrypted blob at a stable URL so you can come back to it across devices. They're complementary, not competitors.&lt;/p&gt;

&lt;p&gt;I've been using &lt;a href="https://www.securetext.cloud/" rel="noopener noreferrer"&gt;SecureText, a free encrypted notepad,&lt;/a&gt; as my persistent scratchpad because it implements exactly the flow above — AES-256-GCM, PBKDF2-SHA-256 at 100k iterations, Web Crypto only — and adds developer-friendly touches like multi-tab notes and &lt;a href="https://www.securetext.cloud/features" rel="noopener noreferrer"&gt;custom URL slugs&lt;/a&gt; instead of random IDs. No account, no tracking, and you can inspect the client-side crypto in your own dev tools.&lt;/p&gt;

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

&lt;p&gt;Never use a password as a key — stretch it with PBKDF2 (or Argon2).&lt;br&gt;
Use authenticated encryption (AES-GCM), fresh IV every time.&lt;br&gt;
If plaintext or keys touch the server, it isn't zero-knowledge.&lt;br&gt;
Audit the client. Open dev tools and confirm what actually gets sent.&lt;br&gt;
If you're building anything that stores user text, run the network tab and ask one question: can the server read this? If yes, your users are trusting you, not math.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Cut Memory Store Tests from 3 Hours to 10 Minutes: 18x Efficiency with pytest + Docker</title>
      <dc:creator>BAOFUFAN</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:04:36 +0000</pubDate>
      <link>https://dev.to/_eb7f2a654e97a60ae9f96e/cut-memory-store-tests-from-3-hours-to-10-minutes-18x-efficiency-with-pytest-docker-55d1</link>
      <guid>https://dev.to/_eb7f2a654e97a60ae9f96e/cut-memory-store-tests-from-3-hours-to-10-minutes-18x-efficiency-with-pytest-docker-55d1</guid>
      <description>&lt;p&gt;At 1 a.m., a colleague frantically pinged me in the group chat: “The cache is polluted again! The test environment’s sessions are all messed up, user data in beta has been tampered with, and the frontend rendered User A’s shopping cart for User B.” I dug in—keys left over on the same Redis instance had mixed up all the test data for the memory store module. That night I manually flushed the cache more than twenty times, re-running verifications after each cleanup, and only dared to sleep when dawn was breaking.&lt;/p&gt;

&lt;p&gt;This wasn’t a one‑off accident. Any project that uses Redis for memory storage—conversation history, user profiles, multi‑turn context—is sitting on a ticking time bomb. If tests and production, or different test cases, share a single Redis instance, cache pollution will blow up sooner or later. This article focuses on one thing: how to use &lt;strong&gt;pytest + Docker&lt;/strong&gt; to build a fully isolated, self‑cleaning, repeatable test environment, turning half‑day debugging marathons into all‑green runs in under 10 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Why Sharing Redis Is a Disaster Waiting to Happen
&lt;/h2&gt;

&lt;p&gt;A typical memory store saves user sessions, LLM conversation history, and short‑term preferences into Redis, using key patterns like &lt;code&gt;memory:user:{uid}:session:{sid}&lt;/code&gt;. During testing we face at least three kinds of pollution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key collisions&lt;/strong&gt; – Multiple test cases that use the same uid/sid cause old keys to overwrite or return dirty data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Residual data&lt;/strong&gt; – A crash mid‑test leaves uncleaned keys in Redis, ruining subsequent runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment drift&lt;/strong&gt; – Local development and CI share one Redis test instance, so different developers’ test cases end up stepping on each other.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The usual “fixes” are to run &lt;code&gt;FLUSHALL&lt;/code&gt; before each test, or to chain a bunch of &lt;code&gt;DELETE&lt;/code&gt; calls in a teardown method. But that’s about as safe as clearing snow with a grenade—you might accidentally wipe out data that shouldn’t be touched, and when multiple developers run tests in parallel, your flush destroys their keys, theirs destroys yours, and everything explodes. Some teams mock Redis with an in‑memory &lt;code&gt;dict&lt;/code&gt;, but a real Redis instance has eviction policies, persistence, and transactional behavior that a mock can’t replicate. You won’t catch real‑world issues like serialization errors, timeouts, or reconnection bugs.&lt;/p&gt;

&lt;p&gt;What we need is: &lt;strong&gt;every test gets a pristine, physically isolated Redis instance that is automatically destroyed when the test finishes&lt;/strong&gt;. Docker is exactly the right tool for this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution Design: Why Use pytest Fixtures Instead of Manually Launching Containers
&lt;/h2&gt;

&lt;p&gt;We considered three approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A – Shared fixed Redis for the team + manual cleanup&lt;/strong&gt; – cheapest, but highest pollution risk. Proven unreliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B – Every developer manually starts a container and stops it after testing&lt;/strong&gt; – consistent environment, yet everyone must remember to start and stop, and it’s a nightmare for CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option C – pytest manages the container lifecycle; fixtures inject clean connections&lt;/strong&gt; – configure once, run anywhere. The container starts before tests begin and is destroyed afterwards, achieving complete isolation. Fixture scopes also let us control reuse granularity (class‑ or function‑level), keeping tests fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We chose Option C. For the tooling we pair &lt;code&gt;pytest&lt;/code&gt; with &lt;code&gt;docker-py&lt;/code&gt; (or the &lt;code&gt;testcontainers&lt;/code&gt; library, which wraps Docker invocations for us). In this article I’ll use &lt;strong&gt;testcontainers-python&lt;/strong&gt; because it already smooths over port‑waiting, health checks, and automatic cleanup.&lt;/p&gt;

&lt;p&gt;A bonus: this approach is agnostic to your business code. No matter where your memory store is defined, as long as it depends on &lt;code&gt;redis.Redis(host=..., port=...)&lt;/code&gt;, we can inject the connection details of a dedicated instance through fixtures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Implementation: Building an Isolated Test Suite from Scratch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Wrap your memory store so it accepts an injectable Redis client&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start by abstracting the memory store to accept an external &lt;code&gt;redis_client&lt;/code&gt; instead of hard‑coding a connection. This is the foundation of testability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# memory_store.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;用户记忆存储，基于 Redis 的 Hash 结构&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:user:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:session:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&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;raw&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Write a fixture with testcontainers that provides a clean Redis instance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code below solves this: on every test run, it automatically pulls the Redis image (if not cached), starts a container on a random port, waits until it’s healthy, hands the connection information to the test, and automatically stops the container when done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RedisContainer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;memory_store&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MemoryStore&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redis_container&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    session 级别容器：整个测试会话只启动一个 Redis 容器，
    但通过每次测试前清理数据来保证隔离。这样启动开销极小。
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RedisContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;
    &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_conta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Practical Guide: Integrating Claude Code with NanoBanana MCP for Image Generation and Editing</title>
      <dc:creator>Germey</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:04:23 +0000</pubDate>
      <link>https://dev.to/germey/practical-guide-integrating-claude-code-with-nanobanana-mcp-for-image-generation-and-editing-8ij</link>
      <guid>https://dev.to/germey/practical-guide-integrating-claude-code-with-nanobanana-mcp-for-image-generation-and-editing-8ij</guid>
      <description>&lt;p&gt;If your AI coding assistant can reason about a UI bug but cannot help you create or adjust the actual image asset, the workflow still breaks at the handoff between code and design.&lt;/p&gt;

&lt;p&gt;Claude Code plus NanoBanana MCP is a practical way to keep that loop inside the terminal. Instead of opening a separate image tool, exporting files, and manually describing the same task again, you can ask Claude Code to generate, edit, or compose images through an MCP server.&lt;/p&gt;

&lt;p&gt;This guide walks through what the integration does, how the MCP connection is configured, and a few developer-focused image workflows that are useful in real projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do
&lt;/h2&gt;

&lt;p&gt;NanoBanana MCP exposes image generation and image editing capabilities to Claude Code.&lt;/p&gt;

&lt;p&gt;The document describes two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nanobanana_generate_image&lt;/code&gt; for text-to-image generation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nanobanana_edit_image&lt;/code&gt; for image editing, including multi-image input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means you can use natural language inside a Claude Code session to do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate a custom illustration for an empty state or 404 page&lt;/li&gt;
&lt;li&gt;remove unwanted text from an image and fill the background&lt;/li&gt;
&lt;li&gt;combine objects from multiple images, such as placing a phone from one image onto a desk in another image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part is not just that images can be generated. NanoBanana is described as using Google's Gemini model for image understanding, which makes it useful for editing instructions where the model needs to understand the relationship between multiple images.&lt;/p&gt;

&lt;p&gt;For builders, this matters because many image tasks are not pure generation. They are usually small workflow tasks: “reuse this object”, “match this scene”, “clean up this screenshot”, or “create an illustration that fits this page”.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The integration is a Claude Code MCP configuration.&lt;/p&gt;

&lt;p&gt;The MCP endpoint is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://nanobanana.mcp.acedata.cloud/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code connects to that endpoint over HTTP and sends an authorization header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: Bearer YOUR_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once connected, Claude Code can call the NanoBanana MCP tools during a terminal session. You still describe what you want in natural language, but the image work is routed through the MCP server instead of staying as plain text advice.&lt;/p&gt;

&lt;p&gt;The core pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code as the terminal interface&lt;/li&gt;
&lt;li&gt;NanoBanana MCP as the connected MCP server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nanobanana_generate_image&lt;/code&gt; for creating images from prompts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nanobanana_edit_image&lt;/code&gt; for editing and multi-image composition&lt;/li&gt;
&lt;li&gt;a Bearer token passed with the &lt;code&gt;Authorization&lt;/code&gt; header&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure NanoBanana MCP in Claude Code
&lt;/h2&gt;

&lt;p&gt;From your terminal, add the MCP server with the &lt;code&gt;claude mcp add&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add nanobanana &lt;span class="nt"&gt;--transport&lt;/span&gt; http https://nanobanana.mcp.acedata.cloud/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_TOKEN&lt;/code&gt; with your own token.&lt;/p&gt;

&lt;p&gt;One small detail is easy to miss: the header flag must be uppercase &lt;code&gt;-H&lt;/code&gt;. Lowercase &lt;code&gt;-h&lt;/code&gt; is help, not a header parameter.&lt;/p&gt;

&lt;p&gt;By default, the configuration is local to the directory where you run the command. The documentation describes three scope options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local&lt;/code&gt;, the default, with no &lt;code&gt;-s&lt;/code&gt; or with &lt;code&gt;-s local&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt;, using &lt;code&gt;-s user&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;project&lt;/code&gt;, using &lt;code&gt;-s project&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;code&gt;local&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt;, the configuration is stored in &lt;code&gt;~/.claude.json&lt;/code&gt;. For &lt;code&gt;project&lt;/code&gt;, it is stored in &lt;code&gt;.mcp.json&lt;/code&gt; in the project root.&lt;/p&gt;

&lt;p&gt;Project scope is useful when a team wants to share the MCP setup through the repository. If you do that, avoid committing real tokens. Use environment variable placeholders instead, so the shared config describes the integration without leaking credentials.&lt;/p&gt;

&lt;p&gt;After adding the server, verify the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;nanobanana&lt;/code&gt; marked as connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Generate a small 404 page illustration
&lt;/h2&gt;

&lt;p&gt;A good first use case is a low-risk asset: an illustration for a 404 page, empty state, or internal tool.&lt;/p&gt;

&lt;p&gt;In Claude Code, you can ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate an illustration of a small dinosaur crouching next to a crack in the ground looking down, for use on a 404 page.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps naturally to &lt;code&gt;nanobanana_generate_image&lt;/code&gt; because the task starts from text only.&lt;/p&gt;

&lt;p&gt;For web projects, this is useful when you need a custom visual that is specific enough to fit the page, but not important enough to justify a long design cycle. You can keep the prompt near the code that needs the asset, iterate in context, and then decide whether the generated image is good enough for the current stage of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: Clean up text in a screenshot
&lt;/h2&gt;

&lt;p&gt;Another common developer task is preparing screenshots for documentation, changelogs, or internal demos.&lt;/p&gt;

&lt;p&gt;If an image contains unwanted text in the bottom-right corner, you can ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit this image to remove the text watermark in the bottom right corner, filling the background.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses &lt;code&gt;nanobanana_edit_image&lt;/code&gt;, because the source image already exists and the task is an edit rather than a new generation.&lt;/p&gt;

&lt;p&gt;The important habit here is to describe both the object to remove and the desired repair behavior. “Remove the text” is less precise than “remove the text watermark in the bottom right corner, filling the background.” The second instruction gives the image model a clearer target and a clearer expectation for the missing area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Compose two images
&lt;/h2&gt;

&lt;p&gt;The most interesting workflow is multi-image composition.&lt;/p&gt;

&lt;p&gt;The documentation gives this kind of prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Put the phone from the first image onto the desk in the second image, adjusting angle and lighting to make it look natural.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also an editing task, but it depends on understanding both images. The model needs to identify the phone in the first image, understand the desk scene in the second image, and then adjust the placement so the result looks coherent.&lt;/p&gt;

&lt;p&gt;This can be handy for product mockups, landing page drafts, documentation visuals, or quick experiments where you want to test whether a concept works before asking a designer to polish it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few practical notes
&lt;/h2&gt;

&lt;p&gt;I would treat this kind of setup as a workflow accelerator, not as a replacement for judgment.&lt;/p&gt;

&lt;p&gt;For production assets, review the final image carefully. For team usage, prefer &lt;code&gt;-s project&lt;/code&gt; only when the configuration is meant to be shared, and keep tokens out of the repository. For one-off experiments, the default local scope is often enough.&lt;/p&gt;

&lt;p&gt;The main advantage is that Claude Code can stay in the same loop as your implementation work. You can reason about a UI state, generate an asset, edit a screenshot, and continue coding without switching tools.&lt;/p&gt;

&lt;p&gt;If you want the original setup reference, the Ace Data Cloud documentation is here: &lt;a href="https://platform.acedata.cloud/documents/claude-code-mcp-nano-banana" rel="noopener noreferrer"&gt;https://platform.acedata.cloud/documents/claude-code-mcp-nano-banana&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Squeezing Every Megabyte: Optimizing an 8GB NVIDIA Jetson Orin Nano for Headless ROS 2 and Edge-AI</title>
      <dc:creator>Md Shaifur Rahman</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:52:26 +0000</pubDate>
      <link>https://dev.to/shaifurcodes/squeezing-every-megabyte-optimizing-an-8gb-nvidia-jetson-orin-nano-for-headless-ros-2-and-edge-ai-3cch</link>
      <guid>https://dev.to/shaifurcodes/squeezing-every-megabyte-optimizing-an-8gb-nvidia-jetson-orin-nano-for-headless-ros-2-and-edge-ai-3cch</guid>
      <description>&lt;p&gt;When building an autonomous robot, compute efficiency is everything. If you are running on an &lt;strong&gt;NVIDIA Jetson Orin Nano (8GB)&lt;/strong&gt;, you are dealing with &lt;strong&gt;Unified Memory (UMA)&lt;/strong&gt;—meaning your CPU and GPU share the exact same physical RAM pool. &lt;/p&gt;

&lt;p&gt;Out of the box, JetPack 7.2 running Ubuntu 24.04 LTS boots a full GNOME graphical desktop that devours &lt;strong&gt;1.5 GB to 2.0 GB of RAM&lt;/strong&gt; just idling. If you are loading an Edge-AI model (like Gemma 2B), running dual IMX219 CSI cameras, processing 2D LiDAR data, and running a ROS 2 navigation stack, that desktop environment is a luxury you cannot afford.&lt;/p&gt;

&lt;p&gt;In this guide, we will walk through a complete, step-by-step engineering log to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Connect and configure a headless Jetson over a direct Ethernet/USB interface.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Systematically strip down the OS to reclaim over 1 GB of precious RAM (bringing idle usage down to ~600 MiB).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install a native, lightweight ROS 2 Jazzy Jalisco environment.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 1: Headless Connection &amp;amp; Setup
&lt;/h2&gt;

&lt;p&gt;When working on a mobile robot, you rarely want an HDMI monitor dangling off your vehicle. Connecting to your Jetson headlessly over a direct Ethernet link or the USB-C device port is the gold standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Hostname Resolution
&lt;/h3&gt;

&lt;p&gt;Ubuntu uses the Avahi mDNS stack. Instead of searching for IP leases in your router's client list, you can connect directly using your Jetson's hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh username@your-jetson-hostname.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Tip: If you are connecting via a direct Ethernet cable between your laptop and the Jetson, ensure your laptop's Ethernet interface is set to "Link-Local Only" or "Share to other computers" to auto-assign compatible IP addresses.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Squeezing Memory for Headless Operations
&lt;/h2&gt;

&lt;p&gt;Let's look at the actual optimization path. Our starting point on a fresh, graphical JetPack 7.2 installation was &lt;strong&gt;913 MiB&lt;/strong&gt; of active RAM usage (with no monitor attached, using SSH). We want to bring this down as low as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Deactivate the Graphical User Interface (GUI)
&lt;/h3&gt;

&lt;p&gt;The single biggest RAM culprit is the graphical environment. Since our robot is autonomous and headless, we can safely instruct systemd to boot directly to a text-only target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set the default system target to multi-user (headless)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl set-default multi-user.target

&lt;span class="c"&gt;# Instantly terminate the active desktop manager session&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl isolate multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Result: Idle memory drops instantly from **913 MiB&lt;/em&gt;* to &lt;strong&gt;699 MiB&lt;/strong&gt;, saving over &lt;strong&gt;200 MiB&lt;/strong&gt;.*&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Ruthlessly Pruning Background Services
&lt;/h3&gt;

&lt;p&gt;Even in headless mode, standard desktop Ubuntu environments run helper programs, network daemons, and background profiling tools that are completely useless on an embedded robot.&lt;/p&gt;

&lt;p&gt;Let's list running services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on this analysis, we can safely disable several heavy-hitting daemons.&lt;/p&gt;

&lt;h4&gt;
  
  
  The "Safe to Disable" List
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lttng-sessiond.service&lt;/code&gt;&lt;/strong&gt;: A kernel tracing tool useful for core OS developers, but unnecessary for robotics applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iperf3.service&lt;/code&gt;&lt;/strong&gt;: A bandwidth benchmarking tool left running as an open port in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;smbd.service&lt;/code&gt; &amp;amp; &lt;code&gt;nmbd.service&lt;/code&gt;&lt;/strong&gt;: Samba file sharing engines designed to host network folders for Windows/macOS network discovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kerneloops.service&lt;/code&gt;&lt;/strong&gt;: Collects and submits crash signatures back to Canonical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bluetooth.service&lt;/code&gt;&lt;/strong&gt;: &lt;em&gt;(Optional)&lt;/em&gt; Unless you are pairing a wireless gamepad directly to the Jetson to steer the car, disable this to free up resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Disable them all in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; lttng-sessiond.service iperf3.service smbd.service nmbd.service kerneloops.service bluetooth.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What We MUST Keep Running
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nvargus-daemon.service&lt;/code&gt;&lt;/strong&gt;: Absolutely essential if you are running CSI cameras (like IMX219s) because it handles the direct hardware Image Signal Processor (ISP) routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nvfancontrol.service&lt;/code&gt;&lt;/strong&gt;: Crucial for active cooling! It dynamically regulates fan speed based on thermal zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jtop.service&lt;/code&gt;&lt;/strong&gt;: The daemon for &lt;code&gt;jetson-stats&lt;/code&gt;—virtually zero footprint, but indispensable for system profiling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 3: Adjust Swappiness and Drop Caches
&lt;/h3&gt;

&lt;p&gt;Because the Jetson uses flash storage (like an NVMe SSD), aggressive swapping can wear down your drive and introduce latency. By default, Linux has a high "swappiness" value (&lt;code&gt;60&lt;/code&gt;), meaning it starts swapping idle memory pages early.&lt;/p&gt;

&lt;p&gt;We want to lower this value to &lt;code&gt;10&lt;/code&gt;, telling the kernel to swap only as an absolute last resort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Temporarily set swappiness to 10&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl vm.swappiness&lt;span class="o"&gt;=&lt;/span&gt;10

&lt;span class="c"&gt;# Make it permanent&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"vm.swappiness=10"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/etc/sysctl.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, you can manually drop inactive system file caches to clear up immediate memory blocks right before loading high-impact AI pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo 3 &amp;gt; /proc/sys/vm/drop_caches'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 3: Native ROS 2 Jazzy Installation
&lt;/h2&gt;

&lt;p&gt;With our OS footprint optimized, we now have a perfect foundation for &lt;strong&gt;ROS 2 Jazzy Jalisco&lt;/strong&gt; (the Tier-1 supported LTS release for Ubuntu 24.04). &lt;/p&gt;

&lt;p&gt;Since we are running a headless robot, we will opt for the &lt;code&gt;ros-base&lt;/code&gt; package installation. This avoids bringing back any desktop GUI tools (like RViz or rqt) which would pull in hundreds of megabytes of heavy X11/Qt visual dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add ROS 2 Repositories
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Ensure locales are configured for UTF-8&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;locales &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;locale-gen en_US en_US.UTF-8
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-locale &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8 &lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;en_US.UTF-8

&lt;span class="c"&gt;# 2. Enable the Ubuntu Universe repository&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;software-properties-common &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository universe &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# 3. Add the ROS 2 GPG key&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;curl &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/ros/rosdistro/master/ros.key &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/ros-archive-keyring.gpg

&lt;span class="c"&gt;# 4. Add the repository to your sources list&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$UBUNTU_CODENAME&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/ros2.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Install ROS 2 Base
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ros-jazzy-ros-base ros-dev-tools &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Source the Environment Permanently
&lt;/h3&gt;

&lt;p&gt;To make the ROS 2 tools available automatically every time you log in via SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"source /opt/ros/jazzy/setup.bash"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify your installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ros2 &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Ultimate Payoff
&lt;/h2&gt;

&lt;p&gt;With all of these steps completed, let's look at our final memory profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mdshaifur@shaifur-orin-nano:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7.4Gi       607Mi       6.2Gi        12Mi       817Mi       6.8Gi
Swap:           15Gi          0B        15Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By transitioning to headless mode and optimizing background services, our baseline memory plummeted to an astonishing &lt;strong&gt;607 MiB&lt;/strong&gt;! &lt;/p&gt;

&lt;p&gt;This leaves you with &lt;strong&gt;6.8 GiB of fully available, ultra-fast unified memory&lt;/strong&gt;. You are now in the perfect position to safely run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM Inference:&lt;/strong&gt; Host local models like Gemma 2B via &lt;code&gt;llama.cpp&lt;/code&gt; using ~1.5–2 GB of VRAM on the Jetson's GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual CSI Cameras:&lt;/strong&gt; Feed high-bandwidth IMX219 streams into your ROS 2 stack without running out of frame buffers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex Robotics Pipelines:&lt;/strong&gt; Execute localization, navigation, and obstacle avoidance simultaneously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimizing your hardware limits isn't just about resource conservation—it is what makes robust, low-latency, and reliable edge robotics possible!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you optimized your Jetson platform for edge-AI or robotics? Let me know your favorite tricks in the comments below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>nvidia</category>
      <category>ros2</category>
      <category>linux</category>
    </item>
    <item>
      <title>"Is it alive?" is the wrong question. Ask "is it working?"</title>
      <dc:creator>chemy_pvl</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:51:22 +0000</pubDate>
      <link>https://dev.to/chemy_pvl/is-it-alive-is-the-wrong-question-ask-is-it-working-5g36</link>
      <guid>https://dev.to/chemy_pvl/is-it-alive-is-the-wrong-question-ask-is-it-working-5g36</guid>
      <description>&lt;h1&gt;
  
  
  "Is it alive?" is the wrong question. The right one is "is it working?"
&lt;/h1&gt;

&lt;p&gt;I run a home lab: a couple of always-on machines, a GPU node, some AI jobs that are supposed to chew through work while I'm asleep or at my day job. The first fear anyone has with unattended infrastructure is simple — &lt;em&gt;if it breaks, will I even notice?&lt;/em&gt; So you add notifications. You put up a dashboard. You get an alert when a process dies. That's a real and necessary step.&lt;/p&gt;

&lt;p&gt;But here's the thing I keep relearning: &lt;strong&gt;monitoring can be green while nothing is getting done.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Last season on the blog I wrote up the incidents from actually running this stuff unattended — the silent stops, the healthy-looking numbers that were lying, the resource contention nobody flagged. The one that reframed everything for me: a GPU node sat idle for &lt;strong&gt;16 hours&lt;/strong&gt;. The process was alive. The network was fine. Every check I had said "OK." The only signal that didn't lie was the one I wasn't watching — &lt;em&gt;the output count wasn't going up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post is the design I landed on, and the checklist I now use before I trust any monitoring setup, mine included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split monitoring into three layers
&lt;/h2&gt;

&lt;p&gt;Most home-lab monitoring collapses into one undifferentiated blob of "checks." It's much easier to reason about — and much easier to find the holes — if you split it into three layers and ask what each one can and cannot tell you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Liveness.&lt;/strong&gt; &lt;em&gt;Is the process alive?&lt;/em&gt; This tells you it crashed, or it restarted. What it misses: the process is running but the work has stalled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Connectivity.&lt;/strong&gt; &lt;em&gt;Does the port open? Does the API respond?&lt;/em&gt; This tells you the thing is reachable from outside. What it misses: it responds, but it isn't doing the job you think it's doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Progress.&lt;/strong&gt; &lt;em&gt;Did the amount of finished work increase over the last unit of time?&lt;/em&gt; This tells you the job is actually moving forward. What it misses: your definition of "output" could be wrong, or the measurement itself could be broken. (That failure mode is real, and I'll come back to it.)&lt;/p&gt;

&lt;p&gt;Liveness is the "notice when it dies" layer. It's the cheapest to add and the one most people already have. Connectivity is the next step up — proving the port is open and the API answers, not just that a PID exists. Both of those are worth having. Neither of them answers the question I actually care about at 2am.&lt;/p&gt;

&lt;p&gt;Because for AI agent workloads and GPU nodes, the interesting failures aren't crashes. They're &lt;em&gt;waits&lt;/em&gt;. Queues that stop draining. Work that blocks on something and never times out. Silent stops where every component is technically healthy and collectively they've achieved nothing.&lt;/p&gt;

&lt;p&gt;Measuring "is it alive" does not tell you "is it working." That gap is the whole reason to bring observability — not just monitoring — into a home lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green dashboards lie, and here's how
&lt;/h2&gt;

&lt;p&gt;Two incidents changed how much I trust a displayed value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capacity showed 99% free while real errors were happening.&lt;/strong&gt; The monitored value said there was plenty of room. Actual operations were failing. The number wasn't wrong when it was written — it was &lt;em&gt;stale&lt;/em&gt;, and stale values get rendered as "normal" by default. Nothing in the display distinguishes "I checked this a second ago and it's fine" from "I last checked this hours ago and it was fine then."&lt;/p&gt;

&lt;p&gt;The rule I took from that: &lt;strong&gt;when a monitored value and a real error contradict each other, believe the real error.&lt;/strong&gt; And structurally, the fix isn't a better threshold — it's a freshness gate. A metric that hasn't been updated recently should not be allowed to count as healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 10G link that only delivered about 3G effective.&lt;/strong&gt; Bandwidth alone looked survivable. What was actually going on showed up somewhere else: the outward-facing port count climbed to roughly 240. That's the pattern worth internalizing — resources usually have a &lt;em&gt;leading&lt;/em&gt; indicator that saturates before the headline number does. If you only watch the headline metric, you find out late.&lt;/p&gt;

&lt;p&gt;So the honest summary is: monitoring starts lying the moment you install it. The value might be old. The notification might not get delivered. The word "OK" on a dashboard might be actively concealing a stalled job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put progress on top of what you already have
&lt;/h2&gt;

&lt;p&gt;I want to be clear that none of this means throwing out your liveness checks or your telemetry pipeline. Both are necessary. The mistake is letting their &lt;em&gt;roles&lt;/em&gt; blur together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liveness monitoring tells you something went down.&lt;/li&gt;
&lt;li&gt;The telemetry pipeline carries observed values from A to B.&lt;/li&gt;
&lt;li&gt;Progress monitoring asks whether the workload is producing results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already have the first two, the realistic move is to add &lt;strong&gt;one&lt;/strong&gt; progress signal on top. Not to rebuild everything. Pick the workload that would hurt most if it silently stalled, define what "one unit of progress" means for it, and alert when that number stops increasing for longer than it should.&lt;/p&gt;

&lt;p&gt;The same skepticism generalizes. Backups are the classic case: the existence of a backup tells you nothing about whether you can restore. Only an actual restore test does. I found broken restores and prune candidates precisely because I stopped trusting the "backup completed" line and went and checked. Monitoring is not a substitute for operating the system — it's an instrument for finding the holes in how you operate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which layers should you actually have?
&lt;/h2&gt;

&lt;p&gt;Depends on what you're running. Roughly how I'd advise choosing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Good fit for&lt;/th&gt;
&lt;th&gt;Bad fit for&lt;/th&gt;
&lt;th&gt;Upside&lt;/th&gt;
&lt;th&gt;Downside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Liveness only&lt;/td&gt;
&lt;td&gt;Anyone with no down-alerts at all today&lt;/td&gt;
&lt;td&gt;Anyone depending on unattended jobs finishing&lt;/td&gt;
&lt;td&gt;Fast to start&lt;/td&gt;
&lt;td&gt;Silent stalls slip through&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liveness + connectivity&lt;/td&gt;
&lt;td&gt;Services consumed from outside&lt;/td&gt;
&lt;td&gt;Anyone who needs to see output increase&lt;/td&gt;
&lt;td&gt;Easy to isolate unreachability&lt;/td&gt;
&lt;td&gt;Confuses "responds" with "produces"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liveness + connectivity + progress&lt;/td&gt;
&lt;td&gt;Unattended AI work, backups, scheduled jobs&lt;/td&gt;
&lt;td&gt;Anyone who can't yet define "progress"&lt;/td&gt;
&lt;td&gt;You can answer "is it working?"&lt;/td&gt;
&lt;td&gt;Needs a per-workload definition of output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add nothing yet&lt;/td&gt;
&lt;td&gt;Still deciding what you run and how you'd respond&lt;/td&gt;
&lt;td&gt;Anyone already trusting unattended work&lt;/td&gt;
&lt;td&gt;Avoids alert noise&lt;/td&gt;
&lt;td&gt;The blind spot stays&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you can't define progress for a workload yet, &lt;strong&gt;don't rush to add more metrics.&lt;/strong&gt; Write down the answer to this instead: &lt;em&gt;what is the moment at which I can say this job succeeded?&lt;/em&gt; Is it that a file appeared? That N items completed? That a restore verification passed? The answer is different for every workload, and metrics you add before you can answer it are decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist: count your own layers
&lt;/h2&gt;

&lt;p&gt;Go count what you have right now.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Liveness&lt;/strong&gt; — if a process or service dies, will I find out? Through a channel I've actually tested, not one I assume works?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity&lt;/strong&gt; — can I confirm the entry points I depend on are reachable &lt;em&gt;and&lt;/em&gt; responding?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress&lt;/strong&gt; — if output or completion count stops increasing for a defined window, will anything tell me?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness&lt;/strong&gt; — can a stale value be displayed as "normal" in my setup? If yes, that's a hole. Gate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction rule&lt;/strong&gt; — when a metric says fine and a real operation fails, do I have the discipline to believe the failure?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leading indicators&lt;/strong&gt; — for my constrained resources, am I only watching the headline number, or the thing that saturates first?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification, not existence&lt;/strong&gt; — for backups and anything else where "it exists" ≠ "it works": have I actually exercised the recovery path?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A missing layer isn't a failure. It's just the next thing to build. No liveness alerting? Start there — it's the cheapest reduction in "didn't notice for 16 hours" risk you can buy. Telemetry that you suspect isn't flowing? Verify the pipeline before you build on it. Liveness and connectivity both green but work still stalling? That's exactly where progress monitoring earns its keep.&lt;/p&gt;

&lt;p&gt;What I want to be left with — and what I'd want for anyone running a lab like mine — isn't &lt;em&gt;having notifications&lt;/em&gt;. It's being able to confirm, at the moment it matters, that things are moving forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full write-up
&lt;/h2&gt;

&lt;p&gt;The full Japanese article, with the incident details, the layer-by-layer breakdown, and the reasoning behind the freshness gate and the connectivity leading indicators, is on the blog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.proto-violet-lab.com/home-lab-monitoring-liveness-connectivity-progress/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto_e0" rel="noopener noreferrer"&gt;「生きているか」ではなく「働いているか」——自宅サーバー監視を実装する / Not "is it alive" but "is it working": implementing home-server monitoring&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>homelab</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>AudioTrust: reconciliar C2PA y watermark AudioSeal en audio sintético</title>
      <dc:creator>Fenix</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:49:54 +0000</pubDate>
      <link>https://dev.to/magopredator/audiotrust-reconciliar-c2pa-y-watermark-audioseal-en-audio-sintetico-1n8f</link>
      <guid>https://dev.to/magopredator/audiotrust-reconciliar-c2pa-y-watermark-audioseal-en-audio-sintetico-1n8f</guid>
      <description>&lt;h1&gt;
  
  
  AudioTrust: reconciliar C2PA y watermark AudioSeal en audio sintético
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Un verificador local que lee las dos marcas de confianza de un audio generado por IA (procedencia C2PA + watermark AudioSeal) y emite un veredicto auditable sobre si coinciden, se contradicen o faltan.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  El problema
&lt;/h2&gt;

&lt;p&gt;Un audio sintético puede llevar dos marcas de confianza distintas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Procedencia C2PA&lt;/strong&gt;: un certificado digital embebido en el archivo (su "DNI" de origen — quién, cuándo, con qué herramienta).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermark AudioSeal&lt;/strong&gt;: un código inaudible incrustado en el sonido, detectable aunque el audio se comparta o transcodifique.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cada una por separado es útil, pero ninguna es suficiente. La procedencia puede faltar (mucho audio generado no la incluye) y el watermark puede estar presente en audio totalmente legítimo. El caso interesante es cuando &lt;strong&gt;se contradicen&lt;/strong&gt;: el manifest C2PA dice "grabado por un humano con una grabadora" pero el watermark de una herramienta de IA está presente. Eso es una señal de manipulación — el llamado &lt;em&gt;Integrity Clash&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;AudioTrust no genera ni firma nada. Es un &lt;strong&gt;verificador&lt;/strong&gt;: lee ambas capas y las reconcilia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;audio.wav  ──►  AudioTrust verify  ──►  veredicto + explicación
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;C2PA&lt;/th&gt;
&lt;th&gt;watermark&lt;/th&gt;
&lt;th&gt;Veredicto&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ausente&lt;/td&gt;
&lt;td&gt;ausente&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unverifiable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ausente&lt;/td&gt;
&lt;td&gt;presente&lt;/td&gt;
&lt;td&gt;&lt;code&gt;partial&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;origen sintético&lt;/td&gt;
&lt;td&gt;presente&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trusted&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;origen humano&lt;/td&gt;
&lt;td&gt;presente&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;contradiction&lt;/code&gt; (Integrity Clash)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Salida JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio.wav"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trusted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"c2pa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"present"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"action=c2pa.created by TestTTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generatedBy=TestTTS"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watermark"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"present"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detect_prob"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C2PA declara origen sintético y hay watermark fuerte: coherentes."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cómo funciona
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lectura C2PA&lt;/strong&gt; con &lt;code&gt;c2pa-python&lt;/code&gt; (el Reader de la librería oficial). Si no hay manifest, devuelve &lt;code&gt;present=False&lt;/code&gt; sin crashear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detección de watermark&lt;/strong&gt; con &lt;code&gt;audioseal&lt;/code&gt;. Devuelve &lt;strong&gt;solo &lt;code&gt;detect_prob&lt;/code&gt;&lt;/strong&gt; (P(audio watermarked) en [0,1]).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliación&lt;/strong&gt; determinista en &lt;code&gt;reconcile.py&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dos decisiones de diseño que vale la pena explicitar porque no son obvias:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nunca se usa el mensaje decodificado de AudioSeal.&lt;/strong&gt; En el spike de validación, el mensaje recuperado fue &lt;em&gt;inconsistente entre corridas&lt;/em&gt; (la primera coincidía con el original, las siguientes no) pese a un &lt;code&gt;detect_prob&lt;/code&gt; estable. Por tanto el producto se apoya solo en &lt;code&gt;detect_prob&lt;/code&gt;, que sí es reproducible. El mensaje se descarta explícitamente en el código.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El origen se deduce de los claims de acción del manifest, no del campo &lt;code&gt;source_type&lt;/code&gt;.&lt;/strong&gt; Verificamos con firmas reales de &lt;code&gt;c2patool&lt;/code&gt; que los manifests C2PA reales &lt;strong&gt;no incluyen &lt;code&gt;source_type&lt;/code&gt; explícito por defecto&lt;/strong&gt; (usan &lt;code&gt;c2pa.actions&lt;/code&gt; con &lt;code&gt;c2pa.created&lt;/code&gt;). Clasificar como "sintético" solo por la presencia de &lt;code&gt;c2pa.created&lt;/code&gt; es un error: esa acción está en casi cualquier manifest, sea IA o humano.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  El bug que casi se cuela
&lt;/h2&gt;

&lt;p&gt;La lógica de reconciliación original clasificaba como &lt;code&gt;synthetic&lt;/code&gt; cualquier claim con &lt;code&gt;c2pa.created&lt;/code&gt; genérico, salvo que el nombre del agente contuviera literalmente "human"/"camera"/"capture". Con un agente humano realista —"Zoom H4n", "Voice Memos"— el origen caía en &lt;code&gt;synthetic&lt;/code&gt;, y si el &lt;code&gt;detect_prob&lt;/code&gt; daba alto, el veredicto era &lt;strong&gt;&lt;code&gt;trusted&lt;/code&gt; en vez de &lt;code&gt;contradiction&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Eso invertía el propósito del producto: &lt;code&gt;trusted&lt;/code&gt; demasiado fácil, &lt;code&gt;contradiction&lt;/code&gt; (el caso que justifica la herramienta) casi inalcanzable.&lt;/p&gt;

&lt;p&gt;La corrección: el default sin evidencia positiva de origen es &lt;code&gt;indeterminate&lt;/code&gt; (→ &lt;code&gt;partial&lt;/code&gt;), nunca &lt;code&gt;synthetic&lt;/code&gt;. "Sintético" exige una señal explícita de generación por IA (&lt;code&gt;generatedBy&lt;/code&gt;, &lt;code&gt;softwareAgent&lt;/code&gt;, &lt;code&gt;trained_algorithmic&lt;/code&gt;...); "humano" exige señal de captura. Así &lt;code&gt;trusted&lt;/code&gt; se gana con evidencia y &lt;code&gt;contradiction&lt;/code&gt; es alcanzable.&lt;/p&gt;

&lt;p&gt;Este bug lo encontró la auditoría independiente en un clone limpio, no el self-report de quien lo escribió. Se añadió un test con el caso "Zoom H4n" para que no vuelva a colarse.&lt;/p&gt;

&lt;h2&gt;
  
  
  El proceso (por qué confío en el resultado)
&lt;/h2&gt;

&lt;p&gt;El repositorio no se escribió de una sentada. Siguió gobernanza de tres partes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spike&lt;/strong&gt; de viabilidad (¿&lt;code&gt;c2pa-python&lt;/code&gt; y &lt;code&gt;audioseal&lt;/code&gt; funcionan de verdad en local? — sí, verificado con 3 corridas en 2 entornos distintos).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDD&lt;/strong&gt;: Constitución → Spec → Tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement&lt;/strong&gt; en rama dedicada &lt;code&gt;feature/audiotrust&lt;/code&gt; (nunca &lt;code&gt;main&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditoría externa&lt;/strong&gt; en clone limpio: encontró el bug de reconciliación arriba.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrección + re-verificación&lt;/strong&gt;, luego &lt;strong&gt;merge a &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; con aprobación explícita.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En cada paso donde apareció un problema real —un falso positivo de watermark en audio limpio, el bug de reconciliación, la limitación de &lt;code&gt;c2patool&lt;/code&gt; con WAV— quedó documentado con evidencia cruda en &lt;code&gt;KNOWN_ISSUES.md&lt;/code&gt;, no escondido bajo tests verdes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que queda abierto (honestamente)
&lt;/h2&gt;

&lt;p&gt;El caso de uso estrella —&lt;code&gt;trusted&lt;/code&gt;/&lt;code&gt;contradiction&lt;/code&gt; con un &lt;strong&gt;archivo de audio real firmado&lt;/strong&gt;— hoy solo está cubierto por tests unitarios con el manifest mockeado. No hay un fixture de audio con manifest C2PA real que dispare esos dos veredictos end-to-end, porque &lt;code&gt;c2patool&lt;/code&gt; 0.9.12 firma imágenes JPG pero no acepta WAV en este entorno (limitación de la build, no del binario). Es trabajo de maduración post-merge, documentado como KI-5, y no bloquea el MVP: &lt;code&gt;partial&lt;/code&gt; y &lt;code&gt;unverifiable&lt;/code&gt; sí están probados con datos 100% reales (watermark AudioSeal real + WAV sin manifest).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruébalo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TORCH_COMPILE_DISABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;TORCHDYNAMO_DISABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1   &lt;span class="c"&gt;# requerido en CPU&lt;/span&gt;
audiotrust verify ruta/al/audio.wav &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los pesos de AudioSeal se descargan en la primera ejecución (repo público, sin token).&lt;/p&gt;

&lt;h2&gt;
  
  
  Enlaces
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/amurlaniakea/audiotrust" rel="noopener noreferrer"&gt;https://github.com/amurlaniakea/audiotrust&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licencia:&lt;/strong&gt; AGPL-3.0-or-later — Pedro Sordo Martínez&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why Your Prompts Fail (And How to Fix Them)</title>
      <dc:creator>Yao Xiao</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:49:41 +0000</pubDate>
      <link>https://dev.to/blobxiaoyao/why-your-prompts-fail-and-how-to-fix-them-1fb6</link>
      <guid>https://dev.to/blobxiaoyao/why-your-prompts-fail-and-how-to-fix-them-1fb6</guid>
      <description>&lt;p&gt;Here is a reliable test: find a prompt that isn't working. Read it carefully. Now ask yourself — at which &lt;em&gt;specific&lt;/em&gt; sentence did the model get permission to do what it did wrong?&lt;/p&gt;

&lt;p&gt;You will almost always find it. A hedged instruction. A missing constraint. An ambiguous scope. The model did not misunderstand you — it followed the most statistically probable interpretation of what you wrote. That interpretation was not the one you intended.&lt;/p&gt;

&lt;p&gt;These are not beginner mistakes. They are structural patterns that reappear at every experience level, because they look reasonable when you write them and only reveal themselves in the output.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Prompts fail because they hand interpretive control to the model on dimensions where you had a specific requirement. Each of the seven mistakes below is a different way of doing that — and each has a specific, testable fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Mistake 1: Placing Critical Instructions in the Middle of the Prompt
&lt;/h2&gt;

&lt;p&gt;Language models process all tokens simultaneously through &lt;strong&gt;attention mechanisms&lt;/strong&gt;, but the effective weight any individual token receives depends heavily on its position. &lt;strong&gt;Instructions near the beginning and end of a prompt receive disproportionately more attention weight than those in the middle.&lt;/strong&gt; This is not a quirk — it is a consequence of how positional embeddings interact with self-attention across long contexts.&lt;/p&gt;

&lt;p&gt;This effect is well-documented. The &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;"Lost in the Middle" study (Stanford / UC Berkeley, 2023)&lt;/a&gt; showed that retrieval accuracy from long-context windows degrades significantly for information placed in the middle — even in capable models. The same mechanism applies to instruction prompts: GPT-4o and Claude 3.5 Sonnet both exhibit measurably lower constraint adherence for instructions buried mid-context compared to those at the leading or trailing position. Open-weight models including DeepSeek-V3 and Llama 3 display the same positional bias — this is not a proprietary model quirk, it is a structural property of the transformer architecture.&lt;/p&gt;

&lt;p&gt;The failure pattern looks like this: a paragraph of background context, then the actual task buried inside it, then more context after. The model produces output that addresses the context and partially ignores the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Lead with the instruction; context follows in labeled fields
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ "Here is some background on our product, our customers are mostly 
   B2B SaaS teams, we launched in 2022 and are targeting mid-market, 
   please write a one-paragraph product overview, keeping in mind we 
   have a technical audience..."

✅ Task: Write a one-paragraph product overview for a B2B SaaS tool.
   Audience: Technical buyers at mid-market companies.
   Context: Launched 2022. Core value: [insert here].
   Constraints: Max 80 words. No jargon above an engineering manager's level.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version cannot bury the task because the task is the first thing written. The context follows in named fields. The model cannot misplace what you have explicitly labeled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: Skipping Role Specification (or Writing a Useless One)
&lt;/h2&gt;

&lt;p&gt;When you omit a role, the model does not operate without one — it uses a blend of every role that has ever been associated with your topic in its training data. For most technical topics, that blend is a statistical average of experts, students, Reddit threads, and instructional content written at varying levels. The average of those distributions is consistently mediocre.&lt;/p&gt;

&lt;p&gt;A role specification narrows the output distribution. It is not decorative. This holds across every current frontier model — GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro — because they all share the same underlying mechanism: probability sampling over a token distribution shaped by training data. In &lt;strong&gt;latent space&lt;/strong&gt; terms, a well-defined role constrains which region of the model's semantic space the output is sampled from. A vague role like "you are an expert" barely shifts the probability mass — the distribution remains nearly as wide as with no role at all. A precise role with domain, experience level, and behavioral note pushes the distribution toward a tighter, more useful cluster of outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake within the mistake&lt;/strong&gt;: people who do specify a role often write one that is too broad to do work. "You are a marketing expert" does not narrow the distribution meaningfully. There are thousands of ways to be a marketing expert, writing at hundreds of different register levels, for dozens of audience types.&lt;/p&gt;

&lt;p&gt;A useful role has three components: &lt;strong&gt;domain&lt;/strong&gt;, &lt;strong&gt;experience signal&lt;/strong&gt;, and &lt;strong&gt;behavioral note&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ "You are a marketing expert."

✅ "You are a direct-response copywriter with 10 years of experience 
   writing B2B email campaigns. You write short, functional sentences.
   You never use superlatives. You lead with the outcome, not the process."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavioral note — "You write short, functional sentences" — is the part most people skip. It is also what governs tone and style more directly than the domain specification. The domain tells the model &lt;em&gt;what it knows&lt;/em&gt;. The behavioral note tells the model &lt;em&gt;how it communicates&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Role = domain + experience signal + behavioral note (all three required)
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Mistake 3: Treating "Context" as Background Filler
&lt;/h2&gt;

&lt;p&gt;Context is the most misunderstood component of prompt structure. Most people provide it as a block of background — company history, product description, general situation — and expect the model to extract what is relevant.&lt;/p&gt;

&lt;p&gt;It will. But "relevant" in the model's interpretation is what is statistically associated with the task type — not what is strategically relevant to your specific situation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effective context is not background. It is the specific information a capable human would need to do this exact task for you, and nothing they could reasonably infer from the task itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are asking for a competitive analysis and you include 300 words of company background the model can see in the task description anyway, you have not provided context — you have provided redundant tokens competing for attention with your actual constraints.&lt;/p&gt;

&lt;p&gt;The practical test: for each sentence of context, ask whether a skilled contractor would need that sentence to do this task, or whether they could infer it from what is already stated. If they could infer it, cut it.&lt;/p&gt;

&lt;p&gt;This is connected to why &lt;a href="https://appliedaihub.org/blog/stop-writing-long-prompts/" rel="noopener noreferrer"&gt;prompt compression improves output quality&lt;/a&gt; — removing low-information context does not lose precision; it concentrates attention on the content that actually constrains the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Context = only what can't be inferred; cut everything else
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Mistake 4: Format Specification That Leaves Room for Interpretation
&lt;/h2&gt;

&lt;p&gt;"Keep it concise" is not a format instruction. It is an invitation for the model to define concise on your behalf. Its definition will differ from yours, vary between runs, and generally land on whichever length felt appropriate given the statistical properties of your topic.&lt;/p&gt;

&lt;p&gt;Format instructions that work are binary: either the output satisfies them or it does not. If your format instruction could be followed by an output you would reject, it is not specific enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before and after:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vague format instruction&lt;/th&gt;
&lt;th&gt;Binary format instruction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keep it concise&lt;/td&gt;
&lt;td&gt;Max 150 words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use a professional tone&lt;/td&gt;
&lt;td&gt;No contractions. No first person. Formal register.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organize clearly&lt;/td&gt;
&lt;td&gt;Three H2 sections: Problem, Evidence, Recommendation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't make it too long&lt;/td&gt;
&lt;td&gt;Output fits in one paragraph, 60–80 words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provide enough detail&lt;/td&gt;
&lt;td&gt;Each claim followed by one supporting data point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The column on the right produces reviewable output. You can check each constraint mechanically. The column on the left produces output that "feels right" to the model — which is not the same as output that is right for your use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Negative format constraints&lt;/strong&gt; — explicitly stating what the output must &lt;em&gt;not&lt;/em&gt; include — are often more valuable than positive ones. They eliminate specific failure modes before they occur. "No preamble" removes the three-sentence wind-up the model adds before answering. "No 'In conclusion'" removes the summary paragraph that restates what was already said. Negative constraints are precise, and they compound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Replace every vague descriptor with a binary, mechanically checkable rule
&lt;/h3&gt;

&lt;p&gt;If you are writing format specifications from scratch, a structured prompt builder removes the guesswork. &lt;a href="https://appliedaihub.org/tools/prompt-scaffold/" rel="noopener noreferrer"&gt;Prompt Scaffold&lt;/a&gt; provides dedicated fields for Format and Negative Constraints — with a live assembled preview so you can verify the final structure before sending. The token counter in the preview panel is a direct signal for whether your format block is over-specified.&lt;/p&gt;

&lt;p&gt;Here is the same format constraint written both ways, with annotations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ❌ Vague — model interprets "professional" and "concise" independently&lt;/span&gt;
Write a professional and concise product summary.

&lt;span class="gh"&gt;# ✅ Binary — each rule is independently verifiable&lt;/span&gt;
Task: Write a product summary.
Format: One paragraph. Max 80 words.        # ← hard length boundary
Tone: No first person. No contractions.     # ← binary style rules
Exclusions: No feature list. No pricing.    # ← negative scope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Author's Comments: The One Format Mistake I See Most
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;In reviewing hundreds of prompts from engineers and writers, there is a single format pattern I encounter constantly: the instruction contains a word count target but not a structure target.&lt;/p&gt;

&lt;p&gt;"Write a 500-word article on X" produces 500 words. But those 500 words could be one long block, or five 100-word paragraphs, or a mix of headers and bullets. The model chooses, and it chooses based on what is statistically common for articles about X — not based on your actual layout requirements.&lt;/p&gt;

&lt;p&gt;Add a structure specification every time you add a length specification. They are different axes of format control, and both are necessary. "500 words, three sections (Problem / Analysis / Recommendation), each section 150–180 words, no bullet points" is a complete format instruction. "500 words" is a token budget with no architectural guidance.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Mistake 5: Using One Prompt for Tasks That Require a Chain
&lt;/h2&gt;

&lt;p&gt;The single-prompt instinct makes sense: you have one goal, you write one prompt, you expect one output. The problem is that complex tasks have internal dependencies — later steps require the output of earlier steps to be evaluated and confirmed before proceeding.&lt;/p&gt;

&lt;p&gt;When you pack a multi-step task into a single prompt, the model generates all steps in one pass. It cannot evaluate the output of step one before beginning step two. Errors compound silently. The final output looks coherent but may be built on a flawed intermediate result that you never had the opportunity to inspect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical signal that you need a chain instead of a single prompt&lt;/strong&gt;: the task contains a phrase like "then," "based on that," "using the above," or "given the results." If the later task is genuinely conditioned on the outcome of an earlier one, they should be separate prompts.&lt;/p&gt;

&lt;p&gt;A simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Single prompt: "Analyze the strengths and weaknesses of this 
   business model, and then write a 300-word pitch that addresses 
   the weaknesses."

✅ Prompt 1: "Identify the three most significant weaknesses in this 
   business model. Output: a numbered list of three items, each with 
   a one-sentence explanation."

   [Review output. Confirm the weaknesses are correctly identified.]

   Prompt 2: "Write a 300-word pitch for this business model. 
   Address each of the following weaknesses directly: [paste output 
   from Prompt 1]."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intermediate review step is not optional overhead — it is the quality gate. You cannot fix an error in the pitch if you do not know whether the weakness analysis was accurate to begin with.&lt;/p&gt;

&lt;p&gt;This is also the foundation of &lt;strong&gt;Chain-of-Thought (CoT)&lt;/strong&gt; prompting — the principle that breaking a task into explicit intermediate steps produces more reliable results than asking for the final answer directly. The difference between a CoT prompt and a multi-step chain is primarily one of control: CoT lets the model generate its own intermediate steps internally; a prompt chain gives &lt;em&gt;you&lt;/em&gt; the review gate between steps. For high-stakes or multi-dependency tasks, the explicit chain wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: If the task contains "then" or "based on that," split it into separate prompts with a review gate between them
&lt;/h3&gt;

&lt;p&gt;The full taxonomy of when to chain, when to use CoT, and how to pass context between steps is covered in detail in the &lt;a href="https://appliedaihub.org/blog/prompt-chaining-how-to-build-ai-workflows/" rel="noopener noreferrer"&gt;prompt chaining patterns guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 6: No Explicit Output Scope
&lt;/h2&gt;

&lt;p&gt;The model has no natural sense of how much output is appropriate. It defaults to what is statistically typical for your task type — which is almost always longer than what you need and structured differently than you require.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output scope&lt;/strong&gt; is a dimension separate from format. Format describes how the output is organized. Scope describes its boundaries: how many items, how many steps, how many alternatives, how deep to go on each.&lt;/p&gt;

&lt;p&gt;Without explicit scope, you get a "complete" answer in the model's sense — one that covers the topic comprehensively — rather than a &lt;em&gt;useful&lt;/em&gt; answer in your sense, which hits only what you actually need.&lt;/p&gt;

&lt;p&gt;Examples of explicit scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Three options only. Do not generate more."&lt;/li&gt;
&lt;li&gt;"List the five most common causes, not an exhaustive list."&lt;/li&gt;
&lt;li&gt;"One paragraph. Stop after the paragraph."&lt;/li&gt;
&lt;li&gt;"Cover only the client-side implementation. Do not address the server-side."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last type — negative scope — is especially useful for technical tasks. "Do not address X" forces the model to stay in the lane you defined rather than expanding into territory you either do not need or will handle separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: State both what to include &lt;em&gt;and&lt;/em&gt; what to exclude — scope requires both boundaries
&lt;/h3&gt;




&lt;h3&gt;
  
  
  Practical Pitfall Avoidance Guide: When the Output Is Consistently Too Long
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If shortening the output is a recurring problem across multiple prompts, the issue is almost never a missing length instruction. It is a missing &lt;em&gt;scope&lt;/em&gt; instruction.&lt;/p&gt;

&lt;p&gt;The model is not writing long output because you forgot to say "be brief." It is writing long output because it is interpreting the task as requiring comprehensive coverage. Give it a narrower task definition, not a shorter word count. "Identify the single most important consideration" produces a shorter output than "be concise about the considerations" — because the first constrains scope, and the second constrains style.&lt;/p&gt;

&lt;p&gt;Style constraints affect word choice. Scope constraints affect what is included. These are not the same lever.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Mistake 7: Iterating Without Diagnosing
&lt;/h2&gt;

&lt;p&gt;When a prompt fails, the natural instinct is to rephrase and resend. This is not iteration — it is random search in the space of possible prompts. Without knowing &lt;em&gt;which component&lt;/em&gt; failed, changing the wording is as likely to introduce new problems as it is to fix the original one.&lt;/p&gt;

&lt;p&gt;Effective prompt debugging treats each component as an independent variable. &lt;strong&gt;When you change multiple components simultaneously, you cannot determine which change produced the improvement&lt;/strong&gt; — which means you cannot apply that learning to the next prompt.&lt;/p&gt;

&lt;p&gt;The diagnostic framework is straightforward. For each failure mode, there is a specific component to target:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output failure&lt;/th&gt;
&lt;th&gt;Component to fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generic, bland, or obvious&lt;/td&gt;
&lt;td&gt;Missing or too-broad &lt;strong&gt;Role&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right topic, wrong angle&lt;/td&gt;
&lt;td&gt;Missing &lt;strong&gt;Goal&lt;/strong&gt; — the output's purpose and audience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technically correct but unusable&lt;/td&gt;
&lt;td&gt;Missing or weak &lt;strong&gt;Context&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong structure or length&lt;/td&gt;
&lt;td&gt;Underspecified &lt;strong&gt;Format&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Includes things it should not&lt;/td&gt;
&lt;td&gt;Missing &lt;strong&gt;negative constraint&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too comprehensive, too long&lt;/td&gt;
&lt;td&gt;Missing &lt;strong&gt;Scope&lt;/strong&gt; limitation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style is off despite correct content&lt;/td&gt;
&lt;td&gt;Missing &lt;strong&gt;few-shot example&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run one change per iteration. If you change Role and Context and Format together, you cannot know which one closed the gap. The signal is in the isolation. When you identify which component was missing, you have also learned something about your mental model of prompt structure — and that learning transfers to the next prompt you write.&lt;/p&gt;

&lt;p&gt;This also applies when evaluating &lt;strong&gt;zero-shot&lt;/strong&gt; vs. &lt;strong&gt;few-shot&lt;/strong&gt; approaches: if you switch from &lt;strong&gt;zero-shot&lt;/strong&gt; to &lt;strong&gt;few-shot&lt;/strong&gt; and add a role and tighten the format all at once, you have no idea which of the three changes produced the improvement. Test one variable. Record what changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: One component per iteration; use a consistent diagnostic table to identify which component to target
&lt;/h3&gt;

&lt;p&gt;If you are building this diagnostic habit across recurring prompt types, a structured template system helps significantly. &lt;a href="https://appliedaihub.org/tools/prompt-vault/" rel="noopener noreferrer"&gt;Prompt Vault&lt;/a&gt; lets you store the working versions of your prompts with component-level labeling — so when you return to a task two weeks later, you can see exactly which Role, Context, and Constraint combination you had validated, rather than reconstructing it from memory. Because it runs entirely in your browser, your calibrated prompt library stays local and private.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Universal Prompting Framework: What All Seven Fixes Have in Common
&lt;/h2&gt;

&lt;p&gt;These seven mistakes are not independent errors. They share a common mechanism: they each hand interpretive control to the model on a dimension where you had a specific requirement.&lt;/p&gt;

&lt;p&gt;When you omit a role, the model interprets what expertise level to use. When you write a vague constraint, the model interprets what "concise" means. When you skip scope, the model interprets how comprehensive the answer should be. Every gap in your prompt is a degree of freedom you are giving the model — and the model will fill that freedom with the most statistically probable response via its &lt;strong&gt;attention mechanisms&lt;/strong&gt; and latent-space sampling, which is rarely the most &lt;em&gt;useful&lt;/em&gt; response for your specific case.&lt;/p&gt;

&lt;p&gt;The prompts that work are not longer. They are more complete. Complete in the sense that every interpretive decision has been made explicitly — by you, in writing — rather than left to the model's statistical defaults.&lt;/p&gt;

&lt;p&gt;When you can read a prompt and find no remaining gap a capable person would need to ask about, the prompt is done. That standard sounds simple. In practice, it takes deliberate review of each component. Build that habit once and it becomes automatic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Golden Checklist — apply before sending any high-stakes prompt:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instruction first.&lt;/strong&gt; Is the core task in the first two lines, before any context?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role is specific.&lt;/strong&gt; Does it name domain + experience level + at least one behavioral note?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every constraint is binary.&lt;/strong&gt; Can each format rule be checked mechanically — pass or fail?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope is bounded.&lt;/strong&gt; Have you stated both what to include &lt;em&gt;and&lt;/em&gt; what to exclude?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One variable at a time.&lt;/strong&gt; If iterating, did you change exactly one component?&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my AI ignore instructions I put in the middle of the prompt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an attention weight problem, not a comprehension problem. Models like GPT-4o and Claude 3.5 Sonnet distribute attention non-uniformly across the context window. Instructions at the leading and trailing positions receive proportionally more weight. The &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;"Lost in the Middle" research&lt;/a&gt; documented this effect specifically. Move your core instruction to the first line of the prompt and repeat the most critical constraint at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a vague prompt and a bad prompt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A vague prompt is imprecise — it leaves multiple valid interpretations open, and the model picks one. A bad prompt is one that actively produces the wrong interpretation. Vagueness is the more common problem, and it is correctable with binary constraints and explicit scope. A bad prompt often contains conflicting instructions or a role that contradicts the task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if I need few-shot examples or just better instructions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Few-shot examples solve a specific problem: when the output style, tone, or structure is difficult to describe precisely in words but easy to demonstrate. If you can fully specify what you want with explicit constraints, examples are unnecessary overhead. If you find yourself writing "write in a style like..." without being able to define that style in rules, that is the signal to switch to a few-shot approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I use Chain-of-Thought prompting vs. a prompt chain?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chain-of-Thought (CoT) is an in-prompt technique — you instruct the model to reason step-by-step before answering. It works well for self-contained reasoning tasks (math, logic, analysis). A prompt chain is a multi-prompt workflow with human review gates between steps. Use CoT when you want the model to show its reasoning within a single response. Use a chain when the output of one step is genuinely conditional on reviewing the output of a prior step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does adding more context sometimes make outputs worse?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;More context increases the total token count without necessarily increasing the information density. If the additional context is background the model can already infer, you are adding noise — competing for attention with the constraints that actually matter. This is the core argument behind prompt compression: a 150-token prompt with high information density consistently outperforms a 600-token prompt padded with inferrable context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the fastest way to improve a failing prompt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Identify the failure type first. Use the diagnostic table in Mistake 7: generic output points to a Role problem; wrong structure points to a Format problem; output that includes things it shouldn't points to a missing negative constraint. Change exactly one component. Resend. Repeat until the failure mode is eliminated.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;For recurring tasks, the component-by-component approach is easier with a structured builder. &lt;a href="https://appliedaihub.org/tools/prompt-scaffold/" rel="noopener noreferrer"&gt;Prompt Scaffold&lt;/a&gt; separates Role, Task, Context, Format, and Constraints into dedicated fields with a live assembled preview — so you can see immediately which field is empty or over-populated. The token count in the preview panel is a useful signal for whether context has drifted into padding territory.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>promptengineering</category>
      <category>llm</category>
      <category>chatgpt</category>
      <category>ai</category>
    </item>
    <item>
      <title>A variable I'd refactored into one function — and kept referencing from another. Python's lazy evaluation hid it, and an AST test finally caught it</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:44:13 +0000</pubDate>
      <link>https://dev.to/susumun/a-variable-id-refactored-into-one-function-and-kept-referencing-from-another-pythons-lazy-4kli</link>
      <guid>https://dev.to/susumun/a-variable-id-refactored-into-one-function-and-kept-referencing-from-another-pythons-lazy-4kli</guid>
      <description>&lt;p&gt;One day the browser automation flow started failing right after plugin updates with &lt;code&gt;NameError: name 'plugin_form_selectors' is not defined&lt;/code&gt; in the post-update "residual check" step.&lt;/p&gt;

&lt;p&gt;The refactor that introduced this had landed back in v1.6.1. The error didn't surface until many rounds later. Reading the code, the cause is obvious in seconds — but nobody hit it for ages, because &lt;strong&gt;Python's lazy evaluation kept the leftover reference hidden&lt;/strong&gt; until exactly the right execution path ran. This post walks through what the bug was and how we structurally prevented its kind via an AST static-analysis test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened — a reference that crossed a scope boundary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;browser_utils.py&lt;/code&gt; has two functions involved: &lt;code&gt;run_browser_update_flow()&lt;/code&gt;, which orchestrates the whole update flow, and &lt;code&gt;browser_update_remaining_plugins()&lt;/code&gt;, which handles only the plugin-update logic. The list of plugin-form selector candidates, &lt;code&gt;plugin_form_selectors&lt;/code&gt;, used to be a local variable inside &lt;code&gt;run_browser_update_flow()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the v1.6.1 refactor — "let's split plugin update into its own function" — we created &lt;code&gt;browser_update_remaining_plugins()&lt;/code&gt; and &lt;strong&gt;moved the &lt;code&gt;plugin_form_selectors&lt;/code&gt; definition into it&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After v1.6.1 refactor
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;browser_update_remaining_plugins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;plugin_form_selectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;        &lt;span class="c1"&gt;# ← defined here
&lt;/span&gt;        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#update-plugins-table-wrap form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form[name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade-plugins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form[action*=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do-plugin-upgrade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.plugins-php form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# ... update logic ...
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_browser_update_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... call to plugin updater ...
&lt;/span&gt;    &lt;span class="nf"&gt;browser_update_remaining_plugins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ★ post-update "residual check" still uses the old local name
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plugin_form_selectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# NameError
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&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="n"&gt;pending_browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "&lt;strong&gt;after updating, make sure no plugin update forms are still visible&lt;/strong&gt;" residual check stayed in &lt;code&gt;run_browser_update_flow()&lt;/code&gt;. During the refactor, the call to extract this loop alongside the update logic — or alternatively to wire it to a different source — got missed. The result: &lt;strong&gt;a piece of code that tried to read a function-local variable from outside that function&lt;/strong&gt;. A simple mistake in shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took several rounds to surface — Python's lazy evaluation
&lt;/h2&gt;

&lt;p&gt;A Python &lt;code&gt;NameError&lt;/code&gt; happens &lt;strong&gt;when that line of code actually executes&lt;/strong&gt;, not when the module is imported. The name &lt;code&gt;plugin_form_selectors&lt;/code&gt; in &lt;code&gt;for selector in plugin_form_selectors:&lt;/code&gt; doesn't need to be &lt;strong&gt;bound at function-definition time&lt;/strong&gt; — Python only tries to resolve it when execution reaches that loop.&lt;/p&gt;

&lt;p&gt;In practical terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App startup: fine (nobody calls the function)&lt;/li&gt;
&lt;li&gt;Sites where SSH-based plugin updates succeed: fine (the browser-update path isn't taken)&lt;/li&gt;
&lt;li&gt;Sites where browser-based updates run: &lt;strong&gt;NameError, the moment that loop is reached&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bug only fires for sites that need browser automation. If you only run SSH-eligible sites, you never see it. Tests didn't cover that path either. That's why it took several rounds before someone hit the trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promote to a module-level constant; reference from both places
&lt;/h2&gt;

&lt;p&gt;The direction is straightforward — &lt;strong&gt;put it somewhere both functions can see&lt;/strong&gt;. Promote to a module-level constant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Top of module
&lt;/span&gt;&lt;span class="n"&gt;PLUGIN_FORM_SELECTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#update-plugins-table-wrap form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form[name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade-plugins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form[action*=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do-plugin-upgrade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.plugins-php form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;browser_update_remaining_plugins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;plugin_form_selectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PLUGIN_FORM_SELECTORS&lt;/span&gt;    &lt;span class="c1"&gt;# bind to existing local name
&lt;/span&gt;    &lt;span class="c1"&gt;# ... update logic ...
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_browser_update_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... call to plugin updater ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# ★ residual check now reads the same constant
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PLUGIN_FORM_SELECTORS&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&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="n"&gt;pending_browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the local name &lt;code&gt;plugin_form_selectors&lt;/code&gt; inside the function is intentional — we want &lt;strong&gt;minimal diff inside the existing function body&lt;/strong&gt;. The internal code reads the same; only the single source-of-truth has moved to module scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why didn't pyflakes / ruff catch this?
&lt;/h2&gt;

&lt;p&gt;"Surely a linter would have flagged this?" is a fair question. pyflakes and ruff do statically detect undefined names via F821. The trap, though, is finer-grained.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;plugin_form_selectors&lt;/code&gt; &lt;strong&gt;was defined as a local variable inside &lt;code&gt;browser_update_remaining_plugins()&lt;/code&gt;&lt;/strong&gt;, so to Python's static-analysis view, it's &lt;strong&gt;a name that exists somewhere in the project&lt;/strong&gt;. When linters see the same name referenced inside &lt;code&gt;run_browser_update_flow()&lt;/code&gt;, they can't distinguish that it's &lt;strong&gt;another function's local variable&lt;/strong&gt;. That's the limit of pyflakes.&lt;/p&gt;

&lt;p&gt;To be precise, pyflakes detects "&lt;strong&gt;not defined anywhere&lt;/strong&gt;" rather than "&lt;strong&gt;not reachable from the current scope&lt;/strong&gt;." If the same name happens to be a local in some other function, it slips through.&lt;/p&gt;

&lt;p&gt;The supplement: &lt;strong&gt;a scope-aware AST static-analysis test that we write ourselves&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AST undefined-name detector — &lt;code&gt;test_browser_undefined_names.py&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We added a test that walks every function in &lt;code&gt;browser_utils.py&lt;/code&gt;, and for each name reference, &lt;strong&gt;actually checks whether that name is reachable from that function's scope&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_no_undefined_names_in_browser_utils&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every name referenced inside a function in browser_utils.py must be
    resolvable to either: a module-level definition, a function argument,
    or a function-local definition. Otherwise: fail.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BROWSER_UTILS_PY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;module_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_module_level_definitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;builtin_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builtins&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;failures&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="n"&gt;func&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;walk_functions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# function args + names assigned inside the function
&lt;/span&gt;        &lt;span class="n"&gt;local_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_local_definitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module_names&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;local_names&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;builtin_names&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name_node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;walk_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&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;name_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineno&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; undefined: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&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="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test &lt;strong&gt;applies the actual scoping rule that "function-local variables aren't visible from other functions"&lt;/strong&gt;, which fills the gap pyflakes leaves. A regression like our &lt;code&gt;plugin_form_selectors&lt;/code&gt; case — &lt;strong&gt;another function's local accidentally referenced&lt;/strong&gt; — would now fail in CI.&lt;/p&gt;

&lt;p&gt;The real implementation handles &lt;code&gt;import&lt;/code&gt; statements, &lt;code&gt;from ... import *&lt;/code&gt;, conditional definitions (&lt;code&gt;if hasattr(...) else ...&lt;/code&gt;), etc. more rigorously than the sketch above, but the core idea is the same: &lt;strong&gt;re-solve name resolution at every function boundary, independently&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons — close refactor leftovers with machine discipline
&lt;/h2&gt;

&lt;p&gt;Three principles to take away:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Refactors that promote a function-local out of its function need a grep companion.&lt;/strong&gt; Whenever you extract or move a function-local variable, &lt;strong&gt;check whether other functions reference that name&lt;/strong&gt; before you move it. Simple but easy to miss in the heat of a refactor. A single grep covers it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python's lazy evaluation hides refactor leftovers for a long time.&lt;/strong&gt; &lt;code&gt;NameError&lt;/code&gt; only fires when execution reaches the line. Rarely-executed paths can hide the bug for months. Especially in code paths with low test coverage. This is a language characteristic — you can't paper over it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope-crossing references that F821 won't catch — write an AST test to catch them.&lt;/strong&gt; Standard linters detect "not defined anywhere" but not "&lt;strong&gt;not reachable from the current scope&lt;/strong&gt;." A scope-aware AST static-analysis test catches the "refactored-into-some-function" reference at CI time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This fix lives in the same family as the &lt;a href="https://en.wpmm.jp/blog/playwright-strict-mode-wp-admin/" rel="noopener noreferrer"&gt;&lt;code&gt;.first&lt;/code&gt; habit and AST test for Playwright strict-mode violation&lt;/a&gt;: both replace human attention with &lt;strong&gt;structural discipline enforced by the machine&lt;/strong&gt;. In long-lived code like a WordPress maintenance tool, accumulating small AST tests like these one at a time visibly &lt;strong&gt;lowers the psychological cost of refactoring&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I went from couch to coder and realized how much I didn't know</title>
      <dc:creator>Kevin German</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:43:38 +0000</pubDate>
      <link>https://dev.to/klgerman/i-went-from-couch-to-coder-and-realized-how-much-i-didnt-know-5a8c</link>
      <guid>https://dev.to/klgerman/i-went-from-couch-to-coder-and-realized-how-much-i-didnt-know-5a8c</guid>
      <description>&lt;p&gt;I've been "the computer guy" my whole life. The one you call when the Wi-Fi is haunted. But I never wrote code until this year, when AI assistants closed the gap and I built a movie-discovery site (&lt;a href="https://flickomatic.com" rel="noopener noreferrer"&gt;flickomatic.com&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Current user count: me and my wife. So imagine my delight when the weekly hosting bill hit $82. And climbing. Here's what I found when I went digging. Four culprits, none of which I'd have guessed.&lt;/p&gt;

&lt;h2&gt;
  
  
  An impossible number
&lt;/h2&gt;

&lt;p&gt;The biggest line item: "ISR Writes: 8.69M ($34.75)." ISR is the scheme where a page renders once and gets served from cache for a week. My site has about 2,000 pages. There's no arithmetic where that becomes 8.69 million.&lt;/p&gt;

&lt;p&gt;Except there is, one sentence deep in the pricing docs: writes are billed in 8 KB chunks. That "8.69M writes" was really 70 GB of payload shoveled into the cache. New question: shoveled by whom?&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bots were inventing infinite versions of my pages
&lt;/h2&gt;

&lt;p&gt;My URLs look like /movie/603/the-matrix, but it turns out the title part was decorative. /movie/603/absolutely-anything rendered the same page, and each variant got cached as a brand-new entry at ~13 billed writes a pop. No human would ever notice. Crawlers, which hoard URLs in every variant they've ever seen, noticed constantly.&lt;/p&gt;

&lt;p&gt;I now know the word "canonicalization": wrong URL gets a permanent redirect to the one true URL. One cached page per movie. Novel concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. I was archiving movie posters by accident
&lt;/h2&gt;

&lt;p&gt;My link-preview cards fetched each movie poster with a "cached fetch," which sounds responsible. What it actually does is save the entire JPEG into the billed cache. About 19 writes per poster, thousands of movies, read back approximately never. A very expensive museum nobody visits. One changed line killed a third of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. My site's biggest customer was my site
&lt;/h2&gt;

&lt;p&gt;The traffic dashboard: homepage 58.7K requests/day, internal API 58.1K. Suspiciously identical. Top visitor identity: something called "node." At, you guessed it, 58.1K.&lt;/p&gt;

&lt;p&gt;"node" is my own server. The homepage loaded its movie list by making a full HTTP request to its own API, once per visit, cached under a key that included every filter combination. My site was DDoSing itself. Politely. On a schedule. And I was paying for both directions.&lt;/p&gt;

&lt;p&gt;If your function invocations outnumber your page views, go find yourself in your own logs before blaming outsiders.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. My domain has a past life
&lt;/h2&gt;

&lt;p&gt;Heavy bot traffic kept hitting /blog/1, /blog/2, pages I never built. Bing's dashboard explained it: my domain was first "discovered" in January 2010. I bought it this year. Someone ran a blog here fifteen years ago and crawlers are still knocking on that dead door. They now get HTTP 410, "Gone," a status code I didn't know existed and now love.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bot zoo
&lt;/h2&gt;

&lt;p&gt;"Bot traffic" turned out to be four different animals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search and AI-assistant crawlers&lt;/strong&gt; (Googlebot, Bingbot, the crawlers behind Claude and ChatGPT search): how anyone might ever FIND my site. Welcome guests. The goal was making their visits cheap, never blocking them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI training scrapers&lt;/strong&gt;: bulk-download everything, send nothing back. Asked to leave via robots.txt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO-tool crawlers&lt;/strong&gt;: AhrefsBot alone was a THIRD of my traffic. It indexes your site to sell reports to marketers. Asked to leave; complied within hours. Most courteous freeloader on the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scraper farm&lt;/strong&gt; on Alibaba Cloud, 51K requests/day, claiming to be Chrome on Windows and also Chrome on a Mac. Pick a lane. Every request shared one identical TLS fingerprint (a "JA4"). Real devices differ at that layer; 51K identical handshakes is one program in costumes. The firewall can challenge by fingerprint: humans pass, scripts fail. Gone in 60 seconds. Most satisfying minute of the month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results, two days later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cache writes: $2.30/day down to $0.75, still falling&lt;/li&gt;
&lt;li&gt;Data transfer: 65 GB/day down to 8&lt;/li&gt;
&lt;li&gt;Compute: down 75%&lt;/li&gt;
&lt;li&gt;Every crawler I actually want: untouched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Back to hobby money. Site still wide open to the bots that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the computer guy learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read what the billing UNITS mean. One sentence explained an impossible number.&lt;/li&gt;
&lt;li&gt;Every URL your site will answer is a tiny liability. I was offering the internet infinite URLs. Free for them, anyway.&lt;/li&gt;
&lt;li&gt;Your own server is a visitor too. It wears a name tag that says "node."&lt;/li&gt;
&lt;li&gt;Domains have exes. Check what they left behind.&lt;/li&gt;
&lt;li&gt;Before changing anything, write down what number should move, and by how much, if your theory is right. I borrowed that habit from people far more experienced than me. It's the difference between fixing something and poking it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: I didn't untangle this alone. I build with an AI coding assistant. I bring the dashboards and make the calls; it brings the patience to explain ISR billing to me twice. Twenty-five years of being everyone's tech guy, and it turns out what I was missing wasn't aptitude. It was a collaborator who never gets tired of my questions.&lt;/p&gt;

&lt;p&gt;The site all this drama was protecting: &lt;a href="https://flickomatic.com" rel="noopener noreferrer"&gt;https://flickomatic.com&lt;/a&gt;. Feedback welcome. I'm told that's how you get better at this.&lt;/p&gt;

</description>
      <category>newbie</category>
      <category>nextjs</category>
      <category>beginners</category>
      <category>vercel</category>
    </item>
    <item>
      <title>Why SnapDOM Beats html2canvas for DOM-to-Image Capture</title>
      <dc:creator>Juan Martin</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:40:20 +0000</pubDate>
      <link>https://dev.to/tinchox5/why-snapdom-beats-html2canvas-for-dom-to-image-capture-14ch</link>
      <guid>https://dev.to/tinchox5/why-snapdom-beats-html2canvas-for-dom-to-image-capture-14ch</guid>
      <description>&lt;p&gt;Capturing a slice of the DOM as an image is one of those deceptively hard problems in frontend engineering. For years, &lt;a href="https://html2canvas.hertzen.com/" rel="noopener noreferrer"&gt;html2canvas&lt;/a&gt; has been the default answer. It works, it's battle-tested, and it's everywhere. But if you've ever pushed it hard — large trees, custom fonts, shadow DOM, or high-DPI exports — you already know where the cracks are. &lt;a href="https://github.com/zumerlab/snapdom" rel="noopener noreferrer"&gt;SnapDOM&lt;/a&gt; is a newer take on the same problem, and in most of the ways that matter for real applications, it's simply a better tool. Here's a technical breakdown of why.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/U7peCdBiFy8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The core architectural difference
&lt;/h2&gt;

&lt;p&gt;html2canvas works by &lt;strong&gt;re-implementing the browser's rendering engine in JavaScript&lt;/strong&gt;. It walks the DOM, reads computed styles, and then manually paints each element onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. That's an enormous surface area to get right: every CSS property, every layout quirk, every edge case in text rendering has to be reproduced by hand. This is why html2canvas ships with a long list of "known unsupported CSS properties" — it can only render what its authors have explicitly taught it to render.&lt;/p&gt;

&lt;p&gt;SnapDOM takes a fundamentally different route. It serializes the target node into an &lt;strong&gt;inlined SVG&lt;/strong&gt; using the &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt; element, then rasterizes that SVG through the browser's &lt;em&gt;native&lt;/em&gt; rendering pipeline. In other words, instead of re-implementing the browser, it hands the work back to the browser. The practical consequence is huge: if the browser can display it, SnapDOM can generally capture it — gradients, filters, blend modes, transforms, and modern CSS features included — without anyone having to write bespoke support for each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Because SnapDOM leans on native rasterization rather than a hand-rolled paint loop in JS, it avoids a large class of expensive per-element JavaScript work. For typical UI captures the difference is very noticeable, and it scales better as the node count grows. html2canvas's cost climbs with the complexity of the tree because every node is processed by its interpreter; SnapDOM's serialize-then-rasterize approach keeps more of the heavy lifting inside optimized browser internals.&lt;/p&gt;

&lt;p&gt;Caching is another big lever. Resolving fonts, images, backgrounds, and computed styles is some of the most expensive work in any capture, and a lot of it is repeated across captures. SnapDOM leans on caching so those resolved resources don't have to be recomputed every time — the second capture of the same UI, and repeated captures in general (think live previews, thumbnails, or exporting many similar cards), get dramatically faster because the heavy resource resolution is reused instead of redone.&lt;/p&gt;

&lt;p&gt;It's also worth noting SnapDOM's output flexibility: you can get an SVG, a raster image, a &lt;code&gt;Blob&lt;/code&gt;, a data URL, a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, or a ready-to-use &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; in a single call. When you don't force a raster round-trip you can keep the result vector-sharp, which matters for retina displays and print.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;snapdom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@zumer/snapdom&lt;/span&gt;&lt;span class="dl"&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;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Multiple output formats from one capture&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;snapdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;svg&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;snapdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toSvg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;snapdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fidelity and modern CSS
&lt;/h2&gt;

&lt;p&gt;This is where the architectural choice pays off the most. Shadow DOM, web components, pseudo-elements, CSS custom properties, and modern layout all tend to "just work" with SnapDOM because they're rendered by the same engine that renders your page. Fonts are a classic pain point with html2canvas; SnapDOM's approach to inlining resources and honoring the real computed styles produces output that matches what the user actually sees far more often. Fewer surprises, fewer manual workarounds, fewer "why does the export look different from the screen" bug reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  SnapDOM vs. native capture APIs and other approaches
&lt;/h2&gt;

&lt;p&gt;It's fair to ask: browsers keep gaining native capture-ish capabilities, and there are other libraries in this space — so why SnapDOM? A few reasons it stays genuinely useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It already supports the html-to-canvas style output.&lt;/strong&gt; You don't lose anything by choosing SnapDOM: it can hand you a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; (and PNG/JPEG/WebP from it) just like the canvas-first tools, while &lt;em&gt;also&lt;/em&gt; giving you SVG, &lt;code&gt;Blob&lt;/code&gt;, data URL, and &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. It's a superset, not a trade-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native APIs solve a different problem.&lt;/strong&gt; Things like &lt;code&gt;getDisplayMedia&lt;/code&gt;/screen capture or the experimental region-capture APIs grab the &lt;em&gt;rendered viewport&lt;/em&gt; through the OS/compositor. They require user permission prompts, capture what's actually on screen (scroll position, overlays, device chrome), and can't cleanly isolate a single off-screen or partially-scrolled node. SnapDOM captures a &lt;em&gt;specific DOM node&lt;/em&gt; deterministically, with no permission dialog, regardless of what's currently visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt; done right.&lt;/strong&gt; The raw "serialize DOM into an SVG &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt;" trick is well known, but doing it correctly — inlining fonts and images, honoring computed styles, handling shadow DOM and cross-origin resources, and producing consistent raster output — is where most hand-rolled attempts fall apart. SnapDOM packages that hard part into something reliable and extensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency across environments.&lt;/strong&gt; Native and OS-level capture varies by browser, platform, and permissions. A DOM-based approach gives you the same result wherever your page runs, which matters for reproducible thumbnails, share images, and automated pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: native capture is great when you want "whatever is on the user's screen," but for "turn &lt;em&gt;this component&lt;/em&gt; into an image, faithfully and programmatically," SnapDOM is the right tool — and it covers the canvas use case on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins and extensibility
&lt;/h2&gt;

&lt;p&gt;A big practical advantage of SnapDOM is that it's built to be &lt;strong&gt;extended&lt;/strong&gt; rather than patched. Its capture pipeline exposes hook points, so you can transform nodes before serialization, adjust or inline resources, tweak the output, and plug in custom behavior without forking the library. That plugin-oriented design means teams can adapt capture behavior to their own components and edge cases instead of waiting for upstream to add a special case.&lt;/p&gt;

&lt;p&gt;html2canvas, by contrast, is essentially a monolithic renderer. Its extension surface is a set of options and callbacks bolted onto a fixed pipeline. When it doesn't support something, your realistic choices are to work around it, monkey-patch it, or fork it. The extensibility model of SnapDOM turns "the library doesn't do X" from a dead end into a solvable problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexibility in real projects
&lt;/h2&gt;

&lt;p&gt;Beyond raw capture, SnapDOM is flexible about &lt;em&gt;how&lt;/em&gt; you integrate it. Options for background color, scaling/DPI, cropping, resource inlining, and choice of output format let you tune it to the task — thumbnails, social share images, PDF pipelines, or pixel-perfect design exports — without stitching together extra tooling. Because the primitive it produces is an SVG, you also get an escape hatch: you can post-process the vector output before rasterizing, something that's awkward at best with a canvas-only approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to large DOM trees
&lt;/h2&gt;

&lt;p&gt;Big, deeply nested trees are the classic stress test for any DOM-to-image tool, and it's an area where SnapDOM is actively getting better. The stable production releases have already landed impressive speedups for large captures, and the work isn't stopping there: the ongoing effort in the dev branch pushes this even further, with genuinely exciting progress on making large-tree captures faster and more efficient. If you have heavy dashboards or dense component trees, this is a project moving in exactly the right direction — and the gains already shipped in stable are substantial today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintainability
&lt;/h2&gt;

&lt;p&gt;There's a subtler, longer-term argument here too. A library that re-implements the browser is, by definition, chasing a moving target: every new CSS feature is potential new work and a new source of divergence. A library that &lt;em&gt;delegates&lt;/em&gt; to the browser inherits new features largely for free. That makes SnapDOM's core smaller and its behavior more predictable to reason about, which is exactly what you want in a dependency you'll be shipping in production for years.&lt;/p&gt;

&lt;p&gt;From a project-health standpoint, SnapDOM is actively developed with a clear, modern codebase and a design that invites contribution through its plugin points. A tighter core with well-defined extension seams is generally easier to maintain, test, and trust than a sprawling engine that must account for the entire CSS specification by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh, capturing modern UI, or you've been fighting html2canvas over fonts, shadow DOM, performance, or CSS support, SnapDOM is the more forward-looking choice. Delegating rendering to the browser instead of reimplementing it gives you better fidelity, better performance, an actual plugin/extensibility story, and a codebase that's easier to maintain over the long haul.&lt;/p&gt;

&lt;p&gt;Same problem, smarter architecture.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>html</category>
      <category>opensource</category>
    </item>
    <item>
      <title>SilentShare — A Browser-Based Peer-to-Peer File Sharing App</title>
      <dc:creator>Meheer Khan</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:40:11 +0000</pubDate>
      <link>https://dev.to/meheer_khan/silentshare-a-browser-based-peer-to-peer-file-sharing-app-3lhg</link>
      <guid>https://dev.to/meheer_khan/silentshare-a-browser-based-peer-to-peer-file-sharing-app-3lhg</guid>
      <description>&lt;p&gt;Have you ever been in a computer lab, classroom, or office where you needed to quickly send a file between your phone and laptop?&lt;/p&gt;

&lt;p&gt;I run into this problem all the time.&lt;/p&gt;

&lt;p&gt;Sometimes there's no USB cable, no pendrive, Bluetooth is painfully slow, or uploading to cloud storage just to download the file on another device feels unnecessary.&lt;/p&gt;

&lt;p&gt;So I decided to build &lt;strong&gt;SilentShare&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SilentShare?
&lt;/h2&gt;

&lt;p&gt;SilentShare is a browser-based peer-to-peer file sharing application that lets you instantly share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📁 Files (up to 50 MB)&lt;/li&gt;
&lt;li&gt;💻 Code snippets&lt;/li&gt;
&lt;li&gt;📝 Text&lt;/li&gt;
&lt;li&gt;🖼️ Images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No installation.&lt;br&gt;
No account.&lt;br&gt;
No server storing your files.&lt;/p&gt;

&lt;p&gt;Your data goes directly from one device to another using &lt;strong&gt;WebRTC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whether you're sending files from your phone to your laptop, between classmates, or across the internet, SilentShare keeps the process simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opens instantly in any browser&lt;/li&gt;
&lt;li&gt;Doesn't require creating an account&lt;/li&gt;
&lt;li&gt;Doesn't upload files to someone else's server&lt;/li&gt;
&lt;li&gt;Works on desktop and mobile&lt;/li&gt;
&lt;li&gt;Feels lightweight and fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on cloud storage, I wanted the browser itself to become the transfer tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;✨ Peer-to-peer file transfer using WebRTC&lt;/p&gt;

&lt;p&gt;📂 File sharing up to &lt;strong&gt;50 MB&lt;/strong&gt; (including ZIP files)&lt;/p&gt;

&lt;p&gt;🔒 Optional end-to-end encrypted rooms using AES-GCM&lt;/p&gt;

&lt;p&gt;📷 QR code invitations with built-in camera scanner&lt;/p&gt;

&lt;p&gt;📊 Live progress, transfer speed, ETA, pause &amp;amp; resume&lt;/p&gt;

&lt;p&gt;🖼️ Preview support for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images&lt;/li&gt;
&lt;li&gt;Audio&lt;/li&gt;
&lt;li&gt;Video&lt;/li&gt;
&lt;li&gt;PDFs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💻 Share code snippets with syntax highlighting&lt;br&gt;
👥 Multi-user rooms (around 5 participants)&lt;br&gt;
🌙 Dark &amp;amp; Light mode&lt;br&gt;
📱 Installable as a Progressive Web App (PWA)&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a room&lt;/li&gt;
&lt;li&gt;Receive a random room code&lt;/li&gt;
&lt;li&gt;Share the code, QR code, or invite link&lt;/li&gt;
&lt;li&gt;Other devices join&lt;/li&gt;
&lt;li&gt;Start sharing instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The files are transferred directly between devices instead of passing through a storage server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;One of the goals of SilentShare was privacy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No user accounts&lt;/li&gt;
&lt;li&gt;No cloud storage&lt;/li&gt;
&lt;li&gt;No permanent database&lt;/li&gt;
&lt;li&gt;Nothing stored after the browser tab closes
If you set a room password, all transferred data is encrypted end-to-end using AES-GCM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;🌐 APP LINK:&lt;br&gt;
&lt;a href="https://silent-share-online.vercel.app" rel="noopener noreferrer"&gt;https://silent-share-online.vercel.app&lt;/a&gt;&lt;/p&gt;

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