<?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>Forem: Ekong Ikpe</title>
    <description>The latest articles on Forem by Ekong Ikpe (@edmundsparrow).</description>
    <link>https://forem.com/edmundsparrow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3576994%2F1f960caa-f3fa-4a0d-bb37-232d2abe2e50.png</url>
      <title>Forem: Ekong Ikpe</title>
      <link>https://forem.com/edmundsparrow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/edmundsparrow"/>
    <language>en</language>
    <item>
      <title>Gnoke-Database: Firebase in your pocket.</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Sat, 09 May 2026 16:49:12 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-database-firebase-in-your-pocket-19a6</link>
      <guid>https://forem.com/edmundsparrow/gnoke-database-firebase-in-your-pocket-19a6</guid>
      <description>&lt;h2&gt;
  
  
  The server doesn't need to be a warehouse. Sometimes it just needs to be a switchboard..
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I built the exit door&lt;/em&gt;. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What Gnoke-Database is:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a complete backend engine — collections, auth, offline sync, roles, identity isolation, OTP recovery — that runs on any PHP host with SQLite.&lt;/p&gt;

&lt;p&gt;Not a cloud service. Not a monthly subscription. A folder you upload once.&lt;/p&gt;

&lt;p&gt;The cheapest shared host you can find. The one that costs less than your data bill. That host is now your Firebase.👍&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What it actually does:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your app saves records locally first. Always. No network required. When the connection returns, Gnoke pushes the queue silently. When a teammate makes a change on their device, Gnoke pulls it down. Your UI reacts. Nobody waited. Nobody lost data.&lt;/p&gt;

&lt;p&gt;This is not a workaround. This is how it should have worked from the beginning.&lt;/p&gt;

&lt;p&gt;Collections are scoped automatically — per user, per branch, per company — without you writing a single access rule by hand. The identity chain handles it. Same user, different app: separate data. Same app, different branch: separate data. No accidental bleed. Ever.&lt;/p&gt;

&lt;p&gt;Roles are defined once in a config file and enforced on every request. Operators save and sync. Managers delete. Admins touch everything. You write the rule once. Gnoke enforces it everywhere.&lt;/p&gt;

&lt;p&gt;Staff changes device. Forgets PIN. OTP recovery re-establishes the chain in under a minute. No data lost. No admin panic. 👌&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The number that matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firebase charges by reads, writes, and storage — and the meter runs whether you're watching or not.  &lt;/p&gt;

&lt;p&gt;Gnoke-Database runs on SQLite. One file on your server. The bill is your hosting fee. Fixed. Predictable. Yours.&lt;/p&gt;

&lt;p&gt;One deployment serves your entire company. Multi-tenant mode gives every client their own isolated database file — same server, zero bleed, independent backups.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What this is not:&lt;/strong&gt;. 😎&lt;/p&gt;

&lt;p&gt;This is not a Firebase killer for Google-scale infrastructure. If you are serving 50 million concurrent users across five continents, you have different problems.&lt;/p&gt;

&lt;p&gt;Most apps are over-engineered by default — hosted on infrastructure designed for companies a hundred times their size, paying for headroom they will never use.&lt;/p&gt;

&lt;p&gt;Gnoke-Database is the right size. Deployable in an afternoon. Owned completely.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The philosophy in one line:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your backend should live on your terms — not on a pricing page you didn't write.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Gnoke-Database — MIT License. Your own Firebase, on your own server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/edmundsparrow/gnoke-database" rel="noopener noreferrer"&gt;https://github.com/edmundsparrow/gnoke-database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fml2mdyh10mpbsfj39okp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fml2mdyh10mpbsfj39okp.png" alt="If I can think, I can breathe, If I can breathe, I can win" width="650" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>gnoke</category>
      <category>sql</category>
      <category>backend</category>
    </item>
    <item>
      <title>Gnoke Persist: Your Browser Has Short-Term Memory Loss. I Fixed It. 👌</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Thu, 07 May 2026 02:15:29 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/your-browser-has-short-term-memory-loss-i-fixed-it-1456</link>
      <guid>https://forem.com/edmundsparrow/your-browser-has-short-term-memory-loss-i-fixed-it-1456</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazrgkww9or0dimlxl48a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazrgkww9or0dimlxl48a.png" alt="Gnoke vibes" width="750" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Have you ever spent an hour typing something into a website, only for your phone to get a notification, refresh the page, and &lt;strong&gt;POOF&lt;/strong&gt;—everything you typed is gone?
&lt;/h2&gt;

&lt;p&gt;That happens because browsers are like Etch-A-Sketches. The moment you "shake" them (refresh or switch apps), they wipe the screen clean. This is especially true on Android, where the system often kills "sleeping" browser tabs to save battery, destroying your unsaved work without warning.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/edmundsparrow/gnoke-persist" rel="noopener noreferrer"&gt;&lt;strong&gt;gnoke-persist&lt;/strong&gt;&lt;/a&gt; to give your browser a permanent memory. It’s a tiny, zero-dependency script that turns your browser into a durable tool.&lt;/p&gt;

&lt;p&gt;Here is how it works (using three tiny "robots"):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Spirit (The Ghost in the Machine) 👻
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Spirit&lt;/strong&gt; is a tiny robot that watches you type. Every time you press a key, it scribbles what you wrote into a secret notebook inside the browser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Magic:&lt;/strong&gt; If the tab dies or you refresh, the Spirit reads its notebook and refills the boxes. This form recovery works across all modern browsers—Safari, Chrome, Firefox, and Edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Privacy Filter:&lt;/strong&gt; The Spirit is helpful, but it tries to be safe. It looks for words like &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, or &lt;code&gt;secret&lt;/code&gt; and ignores those fields. While it’s not a cryptographic vault, it’s designed to keep your secrets out of the recovery log.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Shelf (The Safety Net) 🧺
&lt;/h3&gt;

&lt;p&gt;Usually, if a website tries to save a file to your computer and the battery dies mid-save, that data just disappears. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Shelf&lt;/strong&gt; uses a "Write-Ahead" strategy. Before it even tries to touch your hard drive, it puts a copy of the data on a waiting shelf (IndexedDB). If the browser crashes, the data stays on the shelf. The next time you open the app, the robot sees the item and attempts to finish the delivery for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Bridge (The Key-Holder) 🔑
&lt;/h3&gt;

&lt;p&gt;Browsers are very protective. Even if you give a website permission to a folder, the browser "locks the door" every time you refresh the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Bridge&lt;/strong&gt; remembers where your files live. Even when the door locks, the Bridge stays standing. You don't have to pick your folder all over again; you just tap one button to provide a "user gesture." The Bridge then re-opens the path and flushes any saved work from the &lt;strong&gt;Shelf&lt;/strong&gt; straight onto your computer.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Breakdown of Capabilities
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Chrome / Edge / Brave&lt;/th&gt;
&lt;th&gt;Safari (iOS/Mac)&lt;/th&gt;
&lt;th&gt;Firefox&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spirit (Form Recovery)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Universal&lt;/td&gt;
&lt;td&gt;✅ Universal&lt;/td&gt;
&lt;td&gt;✅ Universal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Shelf (Durable Log)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Local Sync&lt;/td&gt;
&lt;td&gt;✅ Local Sync&lt;/td&gt;
&lt;td&gt;✅ Local Sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Bridge (Disk Write)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Direct to Folder&lt;/td&gt;
&lt;td&gt;❌ Capability Limited&lt;/td&gt;
&lt;td&gt;❌ Capability Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  The Technical Truth (For the Engineers)
&lt;/h3&gt;

&lt;p&gt;If you prefer documentation over metaphors, here is the recovery chain under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Spirit&lt;/strong&gt; = &lt;strong&gt;State Persistence&lt;/strong&gt;: Uses a debounced listener on &lt;code&gt;input&lt;/code&gt; events to sync form values to &lt;strong&gt;IndexedDB&lt;/strong&gt;. It attempts a final sync on &lt;code&gt;visibilitychange&lt;/code&gt;, though durability ultimately depends on IndexedDB commit timing before a process kill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Shelf&lt;/strong&gt; = &lt;strong&gt;Write-Ahead Logging (WAL)&lt;/strong&gt;: Every &lt;code&gt;write()&lt;/code&gt; call is first committed to an IndexedDB object store before being sent to the File System Access API. This provides a durability layer that survives process termination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Bridge&lt;/strong&gt; = &lt;strong&gt;Handle Re-acquisition&lt;/strong&gt;: Manages the &lt;code&gt;FileSystemDirectoryHandle&lt;/code&gt; stored in IndexedDB. It handles the &lt;code&gt;NotAllowedError&lt;/code&gt; by providing an orchestration layer that re-requests permission via a user gesture, then flushes the WAL (the Shelf) to disk.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why should you care?
&lt;/h3&gt;

&lt;p&gt;Most web apps are "flimsy." I wanted to build something &lt;strong&gt;"Durable."&lt;/strong&gt; * &lt;strong&gt;Zero Dependencies:&lt;/strong&gt; A tiny vanilla script. No heavy engines, no cloud required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private &amp;amp; Local:&lt;/strong&gt; Your data stays in your browser and on your disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive Grace:&lt;/strong&gt; It recovers what it can everywhere, and does the heavy lifting where the browser allows it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The vibe is simple: Stop losing work.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Check it out:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/edmundsparrow/gnoke-persist" rel="noopener noreferrer"&gt;github.com/edmundsparrow/gnoke-persist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Demo:&lt;/strong&gt; &lt;a href="https://edmundsparrow.github.io/gnoke-persist" rel="noopener noreferrer"&gt;Click here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;gnoke-persist v0.1.1&lt;/strong&gt; · Edmund Sparrow · MIT 2026&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Gnoke Council — Manual Mode</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Tue, 05 May 2026 05:31:00 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-council-manual-mode-33i6</link>
      <guid>https://forem.com/edmundsparrow/gnoke-council-manual-mode-33i6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpshxez0nvnws0tnizkjt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpshxez0nvnws0tnizkjt.jpg" alt="me, using council" width="720" height="1640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thread. Multiple AIs. Deliberation, not polling.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Most people use AI like this: 🤦&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask one model → get one answer
&lt;/li&gt;
&lt;li&gt;Ask multiple models → compare results
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not thinking. That’s polling.&lt;/p&gt;




&lt;h2&gt;
  
  
  What if the AIs could see each other? 🤔
&lt;/h2&gt;

&lt;p&gt;Not side by side.&lt;br&gt;&lt;br&gt;
Not isolated.  &lt;/p&gt;

&lt;p&gt;But in sequence — where each one reads what the previous one said before responding.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Is
&lt;/h2&gt;

&lt;p&gt;Manual Council is the simplest form of that idea.&lt;/p&gt;

&lt;p&gt;No backend.&lt;br&gt;&lt;br&gt;
No orchestration.&lt;br&gt;&lt;br&gt;
No system doing anything for you.&lt;/p&gt;

&lt;p&gt;Just you — passing context forward.&lt;/p&gt;




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

&lt;p&gt;You act as the router.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send your prompt to the first AI
&lt;/li&gt;
&lt;li&gt;Take its response
&lt;/li&gt;
&lt;li&gt;Paste it into the next AI
&lt;/li&gt;
&lt;li&gt;Repeat
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each model inherits the context of the previous one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flow
&lt;/h2&gt;

&lt;p&gt;You → Claude → Gemini → GPT → Grok → You&lt;/p&gt;

&lt;p&gt;Not parallel.&lt;br&gt;&lt;br&gt;
Not voting.&lt;br&gt;&lt;br&gt;
Sequential awareness.&lt;br&gt;
Coordinated responses&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Later models correct earlier ones
&lt;/li&gt;
&lt;li&gt;Weak assumptions get exposed
&lt;/li&gt;
&lt;li&gt;Ideas evolve instead of resetting
&lt;/li&gt;
&lt;li&gt;Disagreement becomes visible
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output starts feeling worked through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters 🧘
&lt;/h2&gt;

&lt;p&gt;Polling gives variation.&lt;br&gt;&lt;br&gt;
Deliberation gives progression.&lt;/p&gt;

&lt;p&gt;Manual Council:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserves context
&lt;/li&gt;
&lt;li&gt;compounds reasoning
&lt;/li&gt;
&lt;li&gt;enables interaction between models
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Tradeoff
&lt;/h2&gt;

&lt;p&gt;Yes — it’s manual.&lt;/p&gt;

&lt;p&gt;You copy. You paste. You decide the order.&lt;/p&gt;

&lt;p&gt;That friction gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;control
&lt;/li&gt;
&lt;li&gt;visibility
&lt;/li&gt;
&lt;li&gt;understanding
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Design a browser-native persistence system that survives tab death without a backend.&lt;/p&gt;

&lt;p&gt;Pass responses forward exactly as they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Could Become
&lt;/h2&gt;

&lt;p&gt;Automation can come later.&lt;/p&gt;

&lt;p&gt;This already works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Point ✍️
&lt;/h2&gt;

&lt;p&gt;Not which AI is better.&lt;/p&gt;

&lt;p&gt;What happens when they think together?&lt;/p&gt;

&lt;h2&gt;
  
  
  When one model feels off, don’t switch—run a council. 👌
&lt;/h2&gt;

&lt;p&gt;*&lt;em&gt;Live Demo *&lt;/em&gt; &lt;a href="https://edmundsparrow.github.io/gnoke-council" rel="noopener noreferrer"&gt;https://edmundsparrow.github.io/gnoke-council&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/edmundsparrow" rel="noopener noreferrer"&gt;https://github.com/edmundsparrow&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Tabs are apps. The OS just never told the browser 🤷</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Mon, 04 May 2026 11:12:37 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/tabs-are-apps-the-os-just-never-told-the-browser-3k72</link>
      <guid>https://forem.com/edmundsparrow/tabs-are-apps-the-os-just-never-told-the-browser-3k72</guid>
      <description>&lt;h2&gt;
  
  
  You have five tabs open right now.
&lt;/h2&gt;

&lt;p&gt;Binance on tab 1. Gmail on tab 2. Scrabble on tab 3. Excalidraw on tab 4. Tab 5 — &lt;a href="https://edmundsparrow.github.io/gnoke-council" rel="noopener noreferrer"&gt;Gnoke Council&lt;/a&gt; — four AIs deliberating together, you as the human moderator, Claude and Gemini and GPT-4o and Grok each building on what the others said. One HTML file. No backend. No API key. Just a tab.&lt;/p&gt;

&lt;p&gt;You're switching between them like apps. Because that's exactly what they are — apps. Web apps. Running in a browser that was designed to forget them the moment the OS decides to free some RAM.&lt;/p&gt;

&lt;p&gt;And when that happens? Gone. Half-typed message. Lost diagram. Wrecked game state. A deliberation mid-thought.&lt;/p&gt;

&lt;p&gt;That's not a browser limitation. That's a missing abstraction. 🧩&lt;/p&gt;




&lt;h2&gt;
  
  
  The thought that started this 🤔
&lt;/h2&gt;

&lt;p&gt;If a browser can recover a file after a crash, why can't it recover the whole session?&lt;/p&gt;

&lt;p&gt;Not autocomplete. Not &lt;code&gt;localStorage&lt;/code&gt; you wire up yourself every single time. The whole thing — form state, scroll position, focused field — restored silently, before first paint, as if nothing happened.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;gnoke-spirit&lt;/code&gt; does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it isn't 🙄
&lt;/h2&gt;

&lt;p&gt;This isn't &lt;code&gt;localStorage&lt;/code&gt;. That's dumb key-value. You put a string in, you get a string back. You manage everything manually — what to save, when to save, when to clear. Every app reinvents the same plumbing from scratch.&lt;/p&gt;

&lt;p&gt;This isn't browser session restore either. That's passive, unpredictable, scoped to the browser's mood. Clears on hard reload. You can't name it, query it, or kill it deliberately.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually is 😎
&lt;/h2&gt;

&lt;p&gt;A process model for the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. The tab is now a process. It has an identity — &lt;code&gt;pid&lt;/code&gt; defaults to &lt;code&gt;location.pathname&lt;/code&gt;, so each route is its own isolated process. It has memory (IndexedDB). It knows what to persist and what to never touch.&lt;/p&gt;

&lt;p&gt;Kill the tab. Reopen it. It picks up exactly where it left off.&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;await&lt;/span&gt; &lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Tab 1 — its own process&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Tab 2 — isolated, independent&lt;/span&gt;
&lt;span class="c1"&gt;// Kill either. Both come back. Neither knows the other crashed.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What gets persisted. What never does.
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Persisted&lt;/th&gt;
&lt;th&gt;Never&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text inputs&lt;/td&gt;
&lt;td&gt;Passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Textareas&lt;/td&gt;
&lt;td&gt;Tokens / secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Select values&lt;/td&gt;
&lt;td&gt;Auth state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scroll position&lt;/td&gt;
&lt;td&gt;Anything sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active field focus&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sensitive filter isn't optional. It's baked in. You can't accidentally persist a password field. Security by default, not by configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The engineering decisions that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One DB connection, cached for the page lifetime.&lt;/strong&gt;&lt;br&gt;
No repeated &lt;code&gt;indexedDB.open()&lt;/code&gt; calls. One connection, reused. This matters on mobile — battery and latency both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema versioning from day one.&lt;/strong&gt;&lt;br&gt;
Empty migration hook now. But when the state shape changes in v2, existing users don't lose their processes. Most people skip this and regret it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Awaited writes everywhere.&lt;/strong&gt;&lt;br&gt;
The visibility handler — the last write before the OS kills the tab — is &lt;code&gt;await&lt;/code&gt;ed. That's the survival write. It has to land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero dependencies. No build step. 2kb.&lt;/strong&gt;&lt;br&gt;
Drop a script tag. Call &lt;code&gt;wake()&lt;/code&gt;. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nx"&gt;formEl&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;span class="c1"&gt;// Start the spirit. Restores last state immediately.&lt;/span&gt;
&lt;span class="c1"&gt;// pid defaults to location.pathname — each route is its own process.&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;span class="c1"&gt;// Wipe process memory.&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gnokeSpirit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// Returns all active process IDs. Your process table.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where this goes next 👀
&lt;/h2&gt;

&lt;p&gt;Right now each tab is its own isolated process. That's already useful.&lt;/p&gt;

&lt;p&gt;But imagine two tabs coexisting in a split UI — Excalidraw on the left, your notes on the right — each holding its own memory, neither crashing the other out when the OS gets impatient. Drag a tab into the perimeter. It brings its state with it. No reload. No memory loss.&lt;/p&gt;

&lt;p&gt;That's the next layer. The browser as an actual OS. Tabs as actual apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; stores values.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gnoke-spirit&lt;/code&gt; preserves where a user &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Those are different things. One is plumbing. The other is a contract — ship it with any webapp and tabs become resumable. The developer stops thinking about storage. The user never notices. It just works. 🔥&lt;/p&gt;

&lt;p&gt;The browser is an OS. Tabs are apps. &lt;code&gt;gnoke-spirit&lt;/code&gt; is the missing layer between them.&lt;/p&gt;




&lt;p&gt;What do you think — is the browser finally ready to be treated like an OS? Or are we still stuck thinking in pages?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://edmundsparrow.github.io/gnoke-spirit" rel="noopener noreferrer"&gt;edmundsparrow.github.io/gnoke-spirit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/edmundsparrow/gnoke-spirit" rel="noopener noreferrer"&gt;github.com/edmundsparrow/gnoke-spirit&lt;/a&gt; — MIT&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks to &lt;a href="https://dev.to/sylwia-lask"&gt;@sylwialaskowska&lt;/a&gt; whose engagement on the first post gave this legs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuq9tjki7xvooq0swlri5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuq9tjki7xvooq0swlri5.png" alt="Tabs Are Processes" width="700" height="294"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Part of the Gnoke Suite by Edmund Sparrow © 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>discuss</category>
      <category>ux</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Accidentally Wrote a Filesystem Driver. For a Browser. 🤔</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Sun, 03 May 2026 07:04:22 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/i-accidentally-wrote-a-filesystem-driver-for-a-browser-53cd</link>
      <guid>https://forem.com/edmundsparrow/i-accidentally-wrote-a-filesystem-driver-for-a-browser-53cd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2n0x07i58q00fqlj3l61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2n0x07i58q00fqlj3l61.png" alt="Crashing Tabs" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Staring at a bug report that makes no sense.
&lt;/h2&gt;

&lt;p&gt;Data gone. No error. No warning. No stack trace. Just… gone.&lt;/p&gt;

&lt;p&gt;Your app writing files using the File System Access API — which, by the way, is genuinely one of the most exciting things that's happened to the browser in years. A user picks a folder. You write to it. Clean, native, no server involved. The dream, right?&lt;/p&gt;

&lt;p&gt;Except on mobile, the dream has a bad habit of dying quietly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Here's what was actually happening
&lt;/h2&gt;

&lt;p&gt;The browser process got killed in the background. Completely normal — Android does this all the time when memory gets tight. Writable stream was mid-write when it happened.&lt;/p&gt;

&lt;p&gt;No error was thrown. The write just… didn't complete.&lt;/p&gt;

&lt;p&gt;And then you start digging. And you realize — this isn't a bug you can patch. It was a &lt;em&gt;category&lt;/em&gt; of problem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OS kills your process → your in-flight write disappears&lt;/li&gt;
&lt;li&gt;You reopen the app → your file handle is stale&lt;/li&gt;
&lt;li&gt;You fire ten writes at once → the File System Access API doesn't serialize them, so they race, collide, and most of them silently fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three completely different failure modes. All silent. All data loss.&lt;/p&gt;

&lt;p&gt;I fixed them. I wrote a write-ahead buffer, a per-filename queue, an IndexedDB fallback shelf, and a recovery mechanism that replays shelved writes on next wake.&lt;/p&gt;

&lt;p&gt;And then I looked at what I'd built and thought…&lt;/p&gt;

&lt;p&gt;&lt;em&gt;wait.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  This isn't a web feature. This is a kernel problem.
&lt;/h2&gt;

&lt;p&gt;Write ordering. Process lifecycle. I/O durability. Recovery after crash.&lt;/p&gt;

&lt;p&gt;These aren't things you solve in web apps. These are things you solve in &lt;em&gt;operating systems&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Which made me ask a question I couldn't stop thinking about: why did I hit OS-level problems inside a browser?&lt;/p&gt;

&lt;p&gt;The uncomfortable answer: &lt;strong&gt;because the browser is already an OS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not metaphorically. Not "kind of." Actually.&lt;/p&gt;

&lt;p&gt;It has a process manager (tab lifecycle, background kill policies).&lt;br&gt;
It has a filesystem (File System Access API, Origin Private File System).&lt;br&gt;
It has persistent storage (IndexedDB, Cache API).&lt;br&gt;
It has a network stack (fetch, WebSockets, WebRTC).&lt;br&gt;
It has a GPU interface (WebGPU).&lt;br&gt;
It has a concurrency model (Web Workers, SharedArrayBuffer).&lt;br&gt;
It can even run compiled binaries (WebAssembly).&lt;/p&gt;

&lt;p&gt;That's not a list of "web features." That's a kernel feature set. 😳&lt;/p&gt;




&lt;h2&gt;
  
  
  And yet… we're still building like it's 2010
&lt;/h2&gt;

&lt;p&gt;Most frameworks still treat the browser like a dumb UI layer sitting on top of a real backend. State lives on a server. Files live on a server. Compute lives on a server. The browser is just the face.&lt;/p&gt;

&lt;p&gt;But the platform has quietly moved on.&lt;/p&gt;

&lt;p&gt;The browser isn't asking your server for permission anymore. It's got its own filesystem. Its own persistent storage. Its own compute pipeline. Its own process isolation.&lt;/p&gt;

&lt;p&gt;We just haven't caught up to what it actually is.&lt;/p&gt;




&lt;h2&gt;
  
  
  What developers are building instead
&lt;/h2&gt;

&lt;p&gt;Here's the thing that gets me. 🤔&lt;/p&gt;

&lt;p&gt;Look at Electron. Look at Tauri. Look at Capacitor. Look at every "make a web app feel native" framework that's shipped in the last decade.&lt;/p&gt;

&lt;p&gt;What are they all doing? They're wrapping the browser. Giving it a launcher. Giving it a system tray icon. Giving it access to APIs it wasn't "supposed" to have.&lt;/p&gt;

&lt;p&gt;They're building a skin. A controlled launcher for a runtime that already had the OS capabilities — it just hadn't admitted it yet.&lt;/p&gt;

&lt;p&gt;The browser doesn't need a wrapper to be an OS. It &lt;em&gt;is&lt;/em&gt; one. The wrappers exist because we haven't fully accepted that yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment it clicked for me
&lt;/h2&gt;

&lt;p&gt;When I finished gnoke-savenative — my little durability layer for mobile browser writes — I described the architecture to a friend.&lt;/p&gt;

&lt;p&gt;Write-ahead buffer. Fallback shelf. Replay on wake. That's WAL. Write-Ahead Logging. The same durability primitive PostgreSQL uses to survive crashes. Implemented inside a browser tab.&lt;/p&gt;

&lt;p&gt;That's not a web developer problem. That's a systems programming problem.&lt;/p&gt;

&lt;p&gt;And I solved it &lt;em&gt;inside a browser tab.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  So what does this mean practically?
&lt;/h2&gt;

&lt;p&gt;I think we're in a weird transitional moment. The browser runtime is already powerful enough to be treated as a first-class OS — but most developers are still building on top of it like it's a view layer.&lt;/p&gt;

&lt;p&gt;The ones who figure this out first get to build the system services. The filesystem drivers. The process managers. The things that sit below the app layer and make everything else reliable.&lt;/p&gt;

&lt;p&gt;That's where I'm going with the Gnoke Suite. Not apps that run in a browser. A system layer that runs &lt;em&gt;as&lt;/em&gt; the browser.&lt;/p&gt;

&lt;p&gt;It's a subtle difference. But I think it changes everything about how you architect what you build. 🧠&lt;/p&gt;




&lt;p&gt;If you want to see what a browser filesystem driver actually looks like in practice:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/edmundsparrow/gnoke-savenative" rel="noopener noreferrer"&gt;gnoke-savenative on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Demos
&lt;/h3&gt;

&lt;p&gt;🏎️ &lt;strong&gt;&lt;a href="https://edmundsparrow.github.io/gnoke-savenative" rel="noopener noreferrer"&gt;The Native I/O Driver&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Demonstrates raw disk access; permissions reset on refresh by design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🧠 &lt;strong&gt;&lt;a href="https://edmundsparrow.github.io/gnoke-spirit" rel="noopener noreferrer"&gt;The Session Persistence Layer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: How I solve the 'refresh' problem using IndexedDB memory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And if you've hit similar problems — silent write failures, stale handles, mobile process kills — I'd genuinely love to hear about it in the comments. I have a feeling more people have run into this than have written about it.&lt;/p&gt;

&lt;p&gt;— Edmund Sparrow, Gnoke Suite&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Gnoke SaveNative: A Durability Layer for the File System Access API on Mobile Browsers</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Sun, 03 May 2026 05:53:01 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-savenative-a-durability-layer-for-the-file-system-access-api-on-mobile-browsers-1gdg</link>
      <guid>https://forem.com/edmundsparrow/gnoke-savenative-a-durability-layer-for-the-file-system-access-api-on-mobile-browsers-1gdg</guid>
      <description>&lt;h1&gt;
  
  
  I was building offline-first apps for the Gnoke Suite and ran into the same wall every time.
&lt;/h1&gt;

&lt;p&gt;The File System Access API works beautifully — until your phone decides otherwise.&lt;/p&gt;

&lt;p&gt;A background OS kill. A tab reload at the wrong moment. Ten concurrent writes racing for the same stream. Any of these silently drops your data. No error. No warning. Just gone.&lt;/p&gt;

&lt;p&gt;The browser fantasy is: pick a folder, write files, done.&lt;br&gt;&lt;br&gt;
The mobile reality is: your process dies, your handle goes stale, and your writable stream errors out mid-write. 😬&lt;/p&gt;

&lt;p&gt;So I built a survival layer around it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Meet gnoke-savenative
&lt;/h2&gt;

&lt;p&gt;A two-layer write pipeline for browser apps. Native filesystem first, IndexedDB shelf as instant fallback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;gnoke-savenative
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;saveNative&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;gnoke-savenative&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openDB&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;idb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Mount once (user gesture required)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&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;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Write — native first, shelf if it fails&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes.txt&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="s1"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After reload — wake restores handle and auto-flushes shelf&lt;/span&gt;
&lt;span class="nx"&gt;workspace&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;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Survival Loop
&lt;/h2&gt;

&lt;p&gt;Every write has a guaranteed outcome — it's either on disk or in the shelf.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write()
  ↓ native success → file on disk ✓
  ↓ native failure → shelved in IndexedDB

wake() after reload
  ↓ handle restored
  ↓ _flush() drains shelf → file on disk ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writes are never dropped — only delayed. Recovery is automatic. Visibility is optional via hooks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes it mobile-ready 📱
&lt;/h2&gt;

&lt;p&gt;On desktop, the File System Access API mostly just works. On mobile (tested on Infinix Android, Chrome), three things will break a naive implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. OS background kills&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The browser process dies. The handle survives in IndexedDB. But the write that was in-flight is gone. The shelf catches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stale writable streams&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Open a stream, come back after the OS has changed something on disk — the stream errors. Every write goes through a fresh &lt;code&gt;createWritable()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Concurrent write races — this is where most implementations silently fail&lt;/strong&gt; 🤷&lt;br&gt;&lt;br&gt;
The File System Access API does not serialize concurrent writes to the same file. Fire ten writes at once and they fight over the same stream — most fail with no error. gnoke-savenative maintains a per-filename queue so writes process in strict order. This is not retry logic — it's guaranteed ordering backed by a persistent fallback. v0.1.1 was specifically a concurrency patch after stress testing exposed false shelf activations on clean writes.&lt;/p&gt;


&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// pick folder, stash handle&lt;/span&gt;
&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// restore handle, auto-flush shelf&lt;/span&gt;
&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// queued write with shelf fallback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Optional hooks for UI feedback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onWriteFailure&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* shelved */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onFlushProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* draining */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;saveNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onFlushComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* recovered */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;This came out of a real stress test — a Ghost Editor testbench on a real Infinix device, with hard reloads, app switches, and 10 concurrent writes fired at once.&lt;/p&gt;

&lt;p&gt;The pattern is essentially a write-ahead buffer with eventual durability: attempt the native write, fall back to the shelf on failure, replay on wake. The same principle behind WAL in database engines — applied to the browser filesystem. 🧠&lt;/p&gt;

&lt;p&gt;v0.1 proved the shelf worked.&lt;br&gt;&lt;br&gt;
v0.1.1 eliminated false shelf activations under concurrent writes.&lt;br&gt;&lt;br&gt;
After the stress test showed zero shelf activations on clean concurrent writes, it was ready to ship. ✅&lt;/p&gt;


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

&lt;p&gt;👉 &lt;a href="https://github.com/edmundsparrow/gnoke-savenative" rel="noopener noreferrer"&gt;github.com/edmundsparrow/gnoke-savenative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zero dependencies (brings your own &lt;code&gt;openDB&lt;/code&gt;). MIT licensed. Vanilla JS ES module.&lt;/p&gt;

&lt;p&gt;Drop it in any project via CDN — no npm, no build step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ES module --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saveNative&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;https://cdn.jsdelivr.net/gh/edmundsparrow/gnoke-savenative/gnoke-savenative.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openDB&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;https://unpkg.com/idb?module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Or plain script tag — window.saveNative available globally --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/gh/edmundsparrow/gnoke-savenative/gnoke-savenative.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— Edmund Sparrow, Gnoke Suite&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>mobile</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Gnoke flatJSON — My JSON detection logic is now a library you can use.</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Tue, 21 Apr 2026 12:34:04 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-flatjson-my-json-detection-logic-is-now-a-library-you-can-use-21jg</link>
      <guid>https://forem.com/edmundsparrow/gnoke-flatjson-my-json-detection-logic-is-now-a-library-you-can-use-21jg</guid>
      <description>&lt;h2&gt;
  
  
  Gnoke-flatjson
&lt;/h2&gt;

&lt;p&gt;⚠️ Users would import a JSON file and the table would either crash, show &lt;code&gt;[object Object]&lt;/code&gt; in cells, or worse — silently drop entire columns without warning.&lt;/p&gt;

&lt;p&gt;The problem wasn't the table renderer. It was that &lt;strong&gt;JSON has no single shape.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;An API response looks like this:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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;p&gt;A database export looks like this:&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;"people"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"age"&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A spreadsheet export looks like this:&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="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"age"&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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;p&gt;They all mean the same thing. But every shape needs different handling.&lt;/p&gt;




&lt;p&gt;🔧 So I wrote a normalizer — a function that accepts any of these shapes and always returns the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;headers&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;name&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;age&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;rows&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;Alice&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;30&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over time it grew to handle 8 formats: silent column drops, &lt;code&gt;[object Object]&lt;/code&gt; cells, broken row ordering on mixed arrays — all handled at the normalization layer, before any renderer ever sees the data.&lt;/p&gt;

&lt;p&gt;Eventually I realised — this logic deserves its own home.&lt;/p&gt;

&lt;p&gt;So I extracted it.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Meet gnoke-flatjson
&lt;/h2&gt;

&lt;p&gt;One function. Any JSON in. Clean &lt;code&gt;{ headers, rows }&lt;/code&gt; out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;gnoke-flatjson
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flatJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gnoke-flatjson&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flatJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// result.headers — column names&lt;/span&gt;
&lt;span class="c1"&gt;// result.rows    — all cells as strings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or drop it straight into any HTML file — no npm needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/gh/edmundsparrow/gnoke-flatjson/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🗂️ What it handles
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input shape&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Array of objects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{ name: "Alice" }]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array of arrays&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[["name"],["Alice"]]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity-wrapped&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "people": [["name"],["Alice"]] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit headers/rows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ headers:[], rows:[[]] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ name: "Alice" }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array of primitives&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["Alice","Bob"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inconsistent keys&lt;/td&gt;
&lt;td&gt;Missing keys become empty strings, no columns dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested objects&lt;/td&gt;
&lt;td&gt;Stringified as JSON instead of &lt;code&gt;[object Object]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;There's a demo page where you can paste any JSON and see the result instantly — including a rendered table.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://edmundsparrow.github.io/gnoke-flatjson" rel="noopener noreferrer"&gt;edmundsparrow.github.io/gnoke-flatjson&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;✂️ The utility was born from a real app solving a real problem. Gnoke DataForge was the inspiration.&lt;/p&gt;

&lt;p&gt;Zero dependencies. MIT licensed. 68 lines.&lt;/p&gt;

&lt;p&gt;— Edmund Sparrow, Gnoke Suite&lt;/p&gt;

</description>
      <category>data</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Gnoke: Repurposing E-Waste into Zero-Energy Knowledge Devices</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Mon, 20 Apr 2026 05:57:04 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-repurposing-e-waste-into-zero-energy-knowledge-devices-3i48</link>
      <guid>https://forem.com/edmundsparrow/gnoke-repurposing-e-waste-into-zero-energy-knowledge-devices-3i48</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for [Weekend ---&lt;br&gt;
**Challenge: Earth Day Edition&lt;/em&gt;* 🌍&lt;br&gt;&lt;br&gt;
&lt;em&gt;(&lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;https://dev.to/challenges/weekend-2026-04-16&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Gnoke&lt;/strong&gt; turns discarded smartphones into &lt;strong&gt;zero-energy knowledge devices&lt;/strong&gt;—reducing e-waste and eliminating reliance on the cloud.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Gnoke Bible&lt;/strong&gt;, an ultra-lightweight, offline-first digital reader designed to extend the life of old, low-spec devices. Instead of letting aging smartphones become landfill, Gnoke repurposes them into permanent, offline knowledge tools.&lt;/p&gt;

&lt;p&gt;Once loaded, the app runs with:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero data usage 📴
&lt;/li&gt;
&lt;li&gt;Zero server dependency ☁️
&lt;/li&gt;
&lt;li&gt;Minimal battery consumption 🔋
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This eliminates repeated data downloads and reduces reliance on energy-intensive cloud infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://edmundsparrow.github.io/gnoke-bible" rel="noopener noreferrer"&gt;https://edmundsparrow.github.io/gnoke-bible&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;💻 &lt;a href="https://github.com/edmundsparrow/gnoke-bible" rel="noopener noreferrer"&gt;https://github.com/edmundsparrow/gnoke-bible&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Ultra-Low-Resource Development
&lt;/h3&gt;

&lt;p&gt;The entire project was designed and built on a low-end Infinix Android device, targeting the same class of hardware it aims to preserve. This ensured real-world performance optimization for constrained environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Decisions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Vanilla Stack: Built with HTML, CSS, and JavaScript only — no frameworks
&lt;/li&gt;
&lt;li&gt;Offline-First Architecture: Service Workers and localStorage store all content locally
&lt;/li&gt;
&lt;li&gt;Energy Efficiency: Minimal CPU usage and reduced battery drain
&lt;/li&gt;
&lt;li&gt;Device Longevity: Optimized for low RAM to keep older phones usable
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why It Matters (Earth Impact) 🌱
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reduces e-waste by extending device lifespan ♻️
&lt;/li&gt;
&lt;li&gt;Eliminates repeated data downloads, lowering energy consumption ⚡
&lt;/li&gt;
&lt;li&gt;Reduces reliance on cloud servers, lowering carbon footprint 🌍
&lt;/li&gt;
&lt;li&gt;Reduces paper demand by replacing frequently printed materials with a one-time digital download 📚
&lt;/li&gt;
&lt;li&gt;Helps preserve trees by lowering repeated printing of books and documents 🌳
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Best Use of Google Gemini ✨
&lt;/h3&gt;

&lt;p&gt;Google Gemini was used as a lightweight collaborator to refine optimization decisions and improve system efficiency.&lt;/p&gt;




</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Defending Vibe Coding: Why Syntax Might Not Be the Bottleneck Anymore</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Mon, 13 Apr 2026 08:52:18 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/defending-vibe-coding-why-syntax-might-not-be-the-bottleneck-anymore-53lp</link>
      <guid>https://forem.com/edmundsparrow/defending-vibe-coding-why-syntax-might-not-be-the-bottleneck-anymore-53lp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsz3cxr5417op1nsbith.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsz3cxr5417op1nsbith.jpg" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;br&gt;
So many talk about vibe coding and its negative effects or error-prone designs. I got tired of it and felt I should make one more post. 🙂&lt;/p&gt;

&lt;p&gt;There’s been a growing debate in tech about AI-assisted coding and whether people who use it truly “understand” what they’re building.&lt;/p&gt;

&lt;p&gt;From my experience building real offline-first apps, I think this debate is based on an outdated assumption — that you must understand every line of code to be a real builder. 🤔&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Has Changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I started building apps by describing what I wanted, not by writing code.
&lt;/li&gt;
&lt;li&gt;“A Gasap retailers webapp to track daily gas sales ”
&lt;/li&gt;
&lt;li&gt;“An offline reader”
&lt;/li&gt;
&lt;li&gt;“A simple system that stores and retrieves data in the browser” &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI generates the implementation. I test behavior in real usage. Over time, the focus shifted from syntax to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system behavior
&lt;/li&gt;
&lt;li&gt;real-world usefulness
&lt;/li&gt;
&lt;li&gt;whether it solves the problem  🙂&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The real skill is not writing code; it is:
&lt;/h2&gt;

&lt;p&gt;Clearly describing intent and recognizing when the system does not match it.&lt;/p&gt;

&lt;p&gt;If something breaks, I don’t need to understand every line of code to describe the symptom and get it fixed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Has Changed
&lt;/h2&gt;

&lt;p&gt;AI has not removed engineering. It has removed manual construction.&lt;/p&gt;

&lt;p&gt;The effort has shifted to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defining what should exist
&lt;/li&gt;
&lt;li&gt;validating what was generated
&lt;/li&gt;
&lt;li&gt;iterating until behavior matches intent
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Personal Context
&lt;/h2&gt;

&lt;p&gt;My long use of Windows XP and Windows 7 eventually birthed the idea of Gnokestation — making the vibe engineering process feel less like a headache.&lt;/p&gt;

&lt;p&gt;Imagine building and coding an entire operating system from an Android phone. That experience pushed me further into thinking that the bottleneck is no longer syntax, but execution clarity and system design.&lt;/p&gt;

&lt;p&gt;In the same direction, I’ve replaced most of my Play Store apps with Gnoke Apps, and I’m genuinely satisfied with the experience. They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ad-free
&lt;/li&gt;
&lt;li&gt;lightweight
&lt;/li&gt;
&lt;li&gt;offline-first
&lt;/li&gt;
&lt;li&gt;focused on simple tasks
&lt;/li&gt;
&lt;li&gt;no forced logins or password saving
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just clean tools that do exactly what they’re meant to do. 🤷&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Iteration Matters
&lt;/h2&gt;

&lt;p&gt;This approach also affects how I handle more complex systems.&lt;/p&gt;

&lt;p&gt;For example, I had started working on HMI templates, but I stepped back from pushing "Gnoke-OBD2" further because I didn’t yet have a reliable way to thoroughly test the system in real conditions.&lt;/p&gt;

&lt;p&gt;Instead of forcing it out, I paused it until the validation environment is solid enough.&lt;/p&gt;

&lt;p&gt;That’s part of the same principle — build fast, but only ship what you can properly verify.  🚴&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;I don’t see AI as replacing understanding. I see it as replacing typing.&lt;/p&gt;

&lt;p&gt;If I can describe a system clearly, and AI can translate it into working software that I can verify in real use, then the value is not in writing code.&lt;/p&gt;

&lt;p&gt;The value is in building something that works.&lt;/p&gt;

&lt;p&gt;Wishing you a productive working week. ✌️&lt;/p&gt;

&lt;h1&gt;
  
  
  vibecoding #webdev #buildinpublic #automation #gnokestation #gnokeapps
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>deeplearning</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Gnoke-timetravel: find out your date for reincarnation. You might come back in the past. 🥺</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Sat, 04 Apr 2026 04:58:48 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-timetravel-3l6j</link>
      <guid>https://forem.com/edmundsparrow/gnoke-timetravel-3l6j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Gnoke TimeTravel ⏳&lt;br&gt;&lt;br&gt;
A lightweight, browser-based "Cosmic Return Date" calculator. Using a proprietary (and highly questionable) karma engine, this app determines exactly when the universe will allow you to return to Earth based on your life choices and plant-parenting skills.  &lt;/p&gt;

&lt;p&gt;🌟 Features&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Karma Engine: A logic-based quiz that evaluates your cosmic standing.&lt;/li&gt;
&lt;li&gt;Dynamic Calculation: Predicts your return year and "Era" based on your total karma score.&lt;/li&gt;
&lt;li&gt;Gnoke Design System: A clean, cosmic-themed UI with a focus on typography and smooth transitions.&lt;/li&gt;
&lt;li&gt;Dark Mode: System-aware and manual toggle support.&lt;/li&gt;
&lt;li&gt;Offline First: Built as a PWA with a Service Worker for reliable performance anywhere in the galaxy.&lt;/li&gt;
&lt;li&gt;Zero Dependencies: Pure Vanilla JS, HTML, and CSS.&lt;/li&gt;
&lt;li&gt;☕ TEA‑RRIFIC&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://edmundsparrow.github.io/gnoke-timetravel" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/edmundsparrow/gnoke-timetravel" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Frontend: HTML5, CSS3 (Custom Properties/Tokens)&lt;/li&gt;
&lt;li&gt;Logic: Vanilla JavaScript (ES6+)&lt;/li&gt;
&lt;li&gt;State Management: Custom Pub/Sub pattern (state.js)&lt;/li&gt;
&lt;li&gt;Persistence: localStorage for theme preferences.&lt;/li&gt;
&lt;li&gt;Icons: Emoji-based for zero asset overhead.&lt;/li&gt;
&lt;li&gt;Built on: Infinix Hot 12 Play (mobile-first approach)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;Community Favorite — it's delightfully useless, fun, and deterministic.  ☕ TEA‑RRIFIC.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Gnoke Reader — A Universal Offline Document Viewer for Developers Who Work With Files</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Mon, 23 Mar 2026 19:10:26 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/gnoke-reader-a-universal-offline-document-viewer-for-developers-who-work-with-files-26id</link>
      <guid>https://forem.com/edmundsparrow/gnoke-reader-a-universal-offline-document-viewer-for-developers-who-work-with-files-26id</guid>
      <description>&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fym9jimm8b0unjz1hbnhj.jpg" alt=" " width="800" height="351"&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This is part of the Gnoke Suite — offline-first tools for developers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You open a &lt;code&gt;.log&lt;/code&gt; → one app&lt;br&gt;&lt;br&gt;
A &lt;code&gt;.env&lt;/code&gt; file → another&lt;br&gt;&lt;br&gt;
A &lt;code&gt;.csv&lt;/code&gt; → Excel (slow for no reason)&lt;br&gt;&lt;br&gt;
A &lt;code&gt;.diff&lt;/code&gt; → now you're stuck  &lt;/p&gt;

&lt;p&gt;Why are we still doing this in 2026?&lt;/p&gt;

&lt;p&gt;Developers, writers, and technical people constantly deal with raw files — logs, configs, dumps, patches — but there’s no &lt;strong&gt;simple, offline-first, no-setup viewer&lt;/strong&gt; that handles all of it cleanly in one place.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Gnoke Reader&lt;/strong&gt; — a universal offline document viewer that runs 100% in the browser.&lt;/p&gt;

&lt;p&gt;Open it. Drop a file. Read instantly.&lt;/p&gt;

&lt;p&gt;No upload. No account. No server.&lt;/p&gt;

&lt;p&gt;If your file opens in a browser, Gnoke Reader should be able to read it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported formats out of the box:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📝 &lt;strong&gt;Markdown&lt;/strong&gt; — rendered with syntax-highlighted code blocks
&lt;/li&gt;
&lt;li&gt;📕 &lt;strong&gt;PDF&lt;/strong&gt; — paged canvas renderer with keyboard navigation
&lt;/li&gt;
&lt;li&gt;📘 &lt;strong&gt;Word Document (.docx)&lt;/strong&gt; — clean HTML conversion
&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;CSV / TSV&lt;/strong&gt; — sortable table with row numbers and column count
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{ }&lt;/code&gt; &lt;strong&gt;JSON&lt;/strong&gt; — collapsible tree viewer
&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Log files&lt;/strong&gt; — level-aware colour coding (ERROR, WARN, INFO, DEBUG)
&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Config files&lt;/strong&gt; (.ini, .cfg, .conf, .toml) — section + key/value colouring
&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;.env files&lt;/strong&gt; — sensitive values masked by default, tap to reveal
&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Diff / Patch&lt;/strong&gt; — added/removed line colouring with a stats bar
&lt;/li&gt;
&lt;li&gt;🗄️ &lt;strong&gt;SQL&lt;/strong&gt; — keyword syntax highlighting, zero runtime dependencies
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs locally.&lt;/p&gt;

&lt;p&gt;Search, font size control, dark/light mode, and recent files are built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://edmundsparrow.github.io/gnoke-reader" rel="noopener noreferrer"&gt;https://edmundsparrow.github.io/gnoke-reader&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;Drop a file → it opens instantly. No loading screens, no waiting.&lt;/p&gt;

&lt;p&gt;On Android, you can share files directly into Gnoke Reader from your file manager — same way you’d share to WhatsApp or Drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/edmundsparrow/gnoke-reader" rel="noopener noreferrer"&gt;https://github.com/edmundsparrow/gnoke-reader&lt;/a&gt;&lt;br&gt;&lt;br&gt;
A universal offline document viewer — Portable. Private. Offline-first.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The entire app is vanilla HTML, CSS, and JavaScript — no framework, no build step.&lt;/p&gt;

&lt;p&gt;The core is a &lt;strong&gt;plugin registry&lt;/strong&gt; in &lt;code&gt;reader-core.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each format is a self-contained plugin that registers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its file extensions
&lt;/li&gt;
&lt;li&gt;an icon
&lt;/li&gt;
&lt;li&gt;an optional CDN library (only if needed)
&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;render()&lt;/code&gt; function
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a file is opened, Gnoke Reader:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects the extension
&lt;/li&gt;
&lt;li&gt;Lazy-loads any required library (only once)
&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;render()&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. No framework gymnastics. No bundlers. Just the browser doing the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interesting technical decisions
&lt;/h3&gt;

&lt;p&gt;The most interesting parts were the zero-dependency formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL&lt;/strong&gt; — custom character-by-character tokenizer handling strings, comments, and 150+ keywords across DDL, DML, functions, and types
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; — detects sensitive keys (&lt;code&gt;PASSWORD&lt;/code&gt;, &lt;code&gt;SECRET&lt;/code&gt;, &lt;code&gt;TOKEN&lt;/code&gt;, &lt;code&gt;API_KEY&lt;/code&gt;) and masks values by default with tap-to-reveal
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff&lt;/strong&gt; — parses unified diff format to generate a stats bar (files changed, additions, deletions) before rendering
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt; — recursive DOM text-node walker; no indexing, no library, works across every format after render
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Offline First, By Design
&lt;/h2&gt;

&lt;p&gt;Files never leave the device.&lt;/p&gt;

&lt;p&gt;Recent file metadata (name and date only — never content) is stored in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The PWA &lt;code&gt;share_target&lt;/code&gt; means Android users can send files directly into Gnoke Reader without installing anything beyond “Add to Home Screen.”&lt;/p&gt;




&lt;p&gt;Gnoke Reader is basically a universal &lt;strong&gt;“open with…” for developers&lt;/strong&gt; — but offline, private, and instant.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Replacing $5,000 of industrial equipment with a browser tab</title>
      <dc:creator>Ekong Ikpe</dc:creator>
      <pubDate>Sun, 22 Mar 2026 06:30:42 +0000</pubDate>
      <link>https://forem.com/edmundsparrow/replacing-5000-of-industrial-equipment-with-a-browser-tab-3jc6</link>
      <guid>https://forem.com/edmundsparrow/replacing-5000-of-industrial-equipment-with-a-browser-tab-3jc6</guid>
      <description>&lt;h1&gt;
  
  
  Why Your Next Industrial HMI Should Just Be a Browser Tab
&lt;/h1&gt;

&lt;p&gt;A standard industrial signal tower setup can cost around &lt;strong&gt;$15,000&lt;/strong&gt; once you factor in the HMI panel, the physical tower, and the proprietary software licenses that tie them together.&lt;/p&gt;

&lt;p&gt;I rebuilt that same functionality in a browser tab.&lt;/p&gt;

&lt;p&gt;The demo runs on your phone’s flashlight to prove the concept. The same &lt;strong&gt;Web Serial&lt;/strong&gt; and &lt;strong&gt;WebUSB&lt;/strong&gt; APIs that control a smartphone torch can also control real PLCs, relay boards, and industrial signal towers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The $15,000 Problem: Vendor Lock-in
&lt;/h2&gt;

&lt;p&gt;Industrial Human-Machine Interface (HMI) panels are expensive for reasons that have little to do with hardware.&lt;/p&gt;

&lt;p&gt;When you buy one, you are not just buying a screen. You are paying for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proprietary OS:&lt;/strong&gt; A black box that no one else supports
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walled garden:&lt;/strong&gt; A vendor-specific programming environment
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License fatigue:&lt;/strong&gt; Fees per seat, per version, per upgrade
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The downtime trap:&lt;/strong&gt; A 6-8 week lead time when a screen breaks
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the screen always breaks.&lt;/p&gt;

&lt;p&gt;When it does, production stops while you wait for a vendor who knows you have no alternative. That is the real cost - not the unit price, but the forced friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shift: The Browser as a Universal HMI
&lt;/h2&gt;

&lt;p&gt;Modern browsers already ship with APIs that industrial vendors charge thousands of dollars to replicate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Industrial Solution&lt;/th&gt;
&lt;th&gt;Browser Equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Display&lt;/td&gt;
&lt;td&gt;$3,000+ HMI Panel&lt;/td&gt;
&lt;td&gt;Any device with Chrome/Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serial Comms&lt;/td&gt;
&lt;td&gt;Proprietary Driver&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Web Serial API&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB Control&lt;/td&gt;
&lt;td&gt;Custom SDK&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;WebUSB API&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera/Torch&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getUserMedia&lt;/code&gt; + constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline Ops&lt;/td&gt;
&lt;td&gt;Local runtime install&lt;/td&gt;
&lt;td&gt;Service Workers + PWA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;IT ticket + vendor&lt;/td&gt;
&lt;td&gt;Paste a URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;SignalTower&lt;/strong&gt; is a proof of concept built on this shift.&lt;/p&gt;

&lt;p&gt;It is an Andon-style stack light controller (Red, Amber, Green) with four operating modes and twelve timed patterns, installable in seconds on almost any device.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Vanilla JS, No Build Step
&lt;/h2&gt;

&lt;p&gt;To keep the system resilient and "Raspberry Pi friendly", I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero frameworks
&lt;/li&gt;
&lt;li&gt;Zero dependencies
&lt;/li&gt;
&lt;li&gt;Zero build process
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core:&lt;/strong&gt; HTML5, CSS3, Vanilla JS
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence:&lt;/strong&gt; &lt;code&gt;localStorage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware output:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getUserMedia&lt;/code&gt; (torch)
&lt;/li&gt;
&lt;li&gt;Web Audio API (alerts)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;UX:&lt;/strong&gt; Vibration API
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Reliability:&lt;/strong&gt; Service Workers (offline PWA)&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state.js   -&amp;gt; runtime state (single source of truth)
theme.js   -&amp;gt; theme + anti-FOUC
ui.js      -&amp;gt; toasts, modals, status UI
tower.js   -&amp;gt; signal logic + pattern engine
update.js  -&amp;gt; version check
app.js     -&amp;gt; bootstrap + DOM wiring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app.js&lt;/code&gt; is the only file that touches DOM events.&lt;/p&gt;

&lt;p&gt;Everything else exposes a clean API. That makes the system portable, predictable, and easy to drop into embedded environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hardware Trick: Using the Torch
&lt;/h2&gt;

&lt;p&gt;The demo uses the phone’s camera torch as a stand-in for real hardware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;facingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;environment&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVideoTracks&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyConstraints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;No video is recorded or displayed.&lt;/p&gt;

&lt;p&gt;The track is simply a handle to hardware.&lt;/p&gt;

&lt;p&gt;Swap this with a Web Serial write to a relay board, and you are controlling a 24V industrial signal tower with the same logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment: Where This Actually Wins
&lt;/h2&gt;

&lt;p&gt;Because SignalTower is a Progressive Web App (PWA), deployment is trivial.&lt;/p&gt;

&lt;p&gt;When a device fails on the factory floor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace it with any cheap tablet
&lt;/li&gt;
&lt;li&gt;Open the URL
&lt;/li&gt;
&lt;li&gt;Add to home screen
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;The Service Worker loads the app offline, and &lt;code&gt;localStorage&lt;/code&gt; restores the configuration.&lt;/p&gt;

&lt;p&gt;Recovery time drops from weeks to minutes.&lt;/p&gt;

&lt;p&gt;No vendor calls. No licensing. No installation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Safety Question
&lt;/h2&gt;

&lt;p&gt;The obvious concern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Anyone can deploy a control UI from a URL. That is dangerous."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This mixes up interface with control authority.&lt;/p&gt;

&lt;p&gt;In real industrial systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PLC logic&lt;/strong&gt; handles safety decisions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical interlocks&lt;/strong&gt; enforce hard limits
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network isolation&lt;/strong&gt; restricts access
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI is just a surface.&lt;/p&gt;

&lt;p&gt;This approach does not remove safety layers. It removes expensive, proprietary interfaces sitting in front of them.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://edmundsparrow.github.io/gnoke-signaltower" rel="noopener noreferrer"&gt;https://edmundsparrow.github.io/gnoke-signaltower&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/edmundsparrow/gnoke-signaltower" rel="noopener noreferrer"&gt;https://github.com/edmundsparrow/gnoke-signaltower&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;SignalTower also runs inside GnokeStation, a browser-based environment for running multiple industrial tools on a single screen.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;If a browser can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Talk to serial devices
&lt;/li&gt;
&lt;li&gt;Control USB hardware
&lt;/li&gt;
&lt;li&gt;Work offline
&lt;/li&gt;
&lt;li&gt;Run on anything
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the real question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why are we still paying thousands for dedicated HMI panels?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Looking for feedback:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you work in embedded systems, PLCs, or industrial automation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where would this fail in a real deployment?
&lt;/li&gt;
&lt;li&gt;Where could it replace existing systems today?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am especially interested in edge cases and real-world constraints.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo5vovikhxfrj4rztoor.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo5vovikhxfrj4rztoor.jpeg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
