<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vishnu's Blog: The Write Up]]></title><description><![CDATA[I like to build things that can be turned into products or services. This publication is a writeup of those projects so that others can learn and build these projects on their own.]]></description><link>https://blog.v1shnu.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 06:52:22 GMT</lastBuildDate><atom:link href="https://blog.v1shnu.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why I built a NAS last year]]></title><description><![CDATA[Introduction
Over the last 6 months I have moved house, and had multiple parts of my family visit, travelled interstate and suffice to stay with a full time job it has been hectic for me. Yet in the middle of all this I ran into many issues that made...]]></description><link>https://blog.v1shnu.com/why-i-built-a-nas-last-year</link><guid isPermaLink="true">https://blog.v1shnu.com/why-i-built-a-nas-last-year</guid><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Fri, 30 Jan 2026 14:08:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/77IoOu54-g4/upload/f7f8e37aff60a0fe5e5411ac51356d72.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Over the last 6 months I have moved house, and had multiple parts of my family visit, travelled interstate and suffice to stay with a full time job it has been hectic for me. Yet in the middle of all this I ran into many issues that made it painfully obvious that I needed a NAS. My dad’s laptop was having severe issues and the only solution that I could find was to reinstall windows. Say what you want about the cost of macs, this is not something I have needed to do on a mac. Reinstalling meant that I had to back everything up on to one drive (he has a subscription) and then having to reinstall windows and add the files back. This was a solid half day of effort.</p>
<p>It got me thinking if my dad didn’t have a one drive subscription, I’d have to go out and buy a external hard drive and then have to back up over USB. I also noted that we have to have so many subscriptions now, One drive, Apple iCloud, Google One, and so and so on. They are all expensive and increase in price year on year. I wanted to look at a solution that had no ongoing costs, an would allow me to store backups for all of my computers and devices as well as potentially act as control centre for my home.</p>
<h1 id="heading-research">Research</h1>
<p>With that aim in my mind I started researching. My initial idea was to buy a Synology NAS. I have come to appreciate the ease of close systems as long as they allow me to do what I want. I use a mac and iPhone, because its easy. I don’t have to worry about things breaking randomly, or drivers failing. Yet Synology was not the answer. They have been mired in controversy recently as they have been pushing updates to their systems to only allow hard drives with Synology firmware. It was a clear an egregious attempt at vendor lock in. As I said I don’t mind if the convenience outweighs the cost, but if you look at the prices $450 AUD for a Synology 4TB hard drive was $150 AUD for the Seagate 4TB hard drive, this is, excuse my Australian, clearly taking the piss.</p>
<p>Synology was out initially on a economic front an even though they have reversed this policy, I cannot trust them to bring it back later, so they are out on a moral front as well.</p>
<p>So I continued my search there are boxes from Ubiquiti, Ugreen and others, yet they all had issues in one way or another, whether its the quality of the product, software support or other limitations. In the end I decided to abandon prebuilts and look at building my own NAS. As I googled around I came across a build that I liked.</p>
<blockquote>
<p>Brian McMoses builds NASs every year. He has put a good deal of thought into the choices he makes. I chose the 2025 version as a base for mine.</p>
<p><a target="_blank" href="https://blog.briancmoses.com/2024/11/diy-nas-2025-edition.html">https://blog.briancmoses.com/2024/11/diy-nas-2025-edition.html</a></p>
</blockquote>
<h1 id="heading-building-the-nas">Building the NAS</h1>
<p>I chose to use 4TB drives which I got for $113 which at the time and even more so today is a great bargain. The small form factor is an added advantage as I need it to be easily movable. The other choices i made were to only go for 16GB of ram and I went with an i3 rather than the n100. The reasons for such? It has better performance and for ram? Cost. For the same reasons I didn’t use an app drive instead I partitioned the os drive to act as an app drive. This saved cost but introduces risk for me.</p>
<p>I chose to install TrueNas Scale, over all the other options out there. This is open source software and also free. I chose it over Open Media Vault and HexOS for both cost, openness, ease of use and feature availability reasons. I am very happy with the choice.</p>
<blockquote>
<p>I used this method to partition the NVME drive to support the boot partition and apps partition on one drive. It is not recommended and not supported but as with al my project I want to learn as much as I can.</p>
<p><a target="_blank" href="https://www.reddit.com/r/truenas/comments/lgf75w/scalehowto_split_ssd_during_installation/?share_id=7FNx79h5XJcC9OYsxSdcT">https://www.reddit.com/r/truenas/comments/lgf75w/scalehowto_split_ssd_during_installation/?share_id=7FNx79h5XJcC9OYsxSdcT</a></p>
</blockquote>
<p>My overall cost for this project can be seen in the table below.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Price (AUD)</td><td>Price (USD)</td></tr>
</thead>
<tbody>
<tr>
<td>Case</td><td>150</td><td>105</td></tr>
<tr>
<td>Motherboard + CPU + NVME + RAM</td><td>521</td><td>365</td></tr>
<tr>
<td>Hard Drives 4TB x4</td><td>453</td><td>317</td></tr>
<tr>
<td>Misc Cables</td><td>50</td><td>35</td></tr>
<tr>
<td>Power supply</td><td>159</td><td>112</td></tr>
<tr>
<td></td><td></td><td></td></tr>
<tr>
<td>Total</td><td>1333</td><td>932</td></tr>
</tbody>
</table>
</div><p>Overall it was relatively cheap for today. I recommend buying the motherboard of Aliexpress but everything else from local stores during sales. The stores tend to have great deals.</p>
<h1 id="heading-issues">Issues</h1>
<p>I had one annoying issue. The hard drive bay fan ran full tilt all the time. To solve it I disconnected it. It isn;t noisy anymore. This is a well known issue this board. I probably would have bought a different one if I had done a bit more research. The other major issue is that the board only has 6 slots for HDDs, 1 NVME and 1 SSD. This limits expansion possibilities but that is fine.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Overall I am very happy with my NAS and it has been running for 3-4 months now. I have deployed applications like Jellyfin, and tailscale on it. These though will be the topic of another post.</p>
]]></content:encoded></item><item><title><![CDATA[Vibe Coding Take 2]]></title><description><![CDATA[After realising my mistakes from my last attempt at vibe coding, which I will henceforth call Ai assisted development. I tackled the same project again. I wanted to build an app that could detect all urls and javascript files on the page separately.
...]]></description><link>https://blog.v1shnu.com/vibe-coding-take-2</link><guid isPermaLink="true">https://blog.v1shnu.com/vibe-coding-take-2</guid><category><![CDATA[vibe coding]]></category><category><![CDATA[AI]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[chrome extension]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Thu, 01 May 2025 00:50:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746056411740/9041f6c4-1976-4763-884b-9b0af30c4357.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After realising my mistakes from my last attempt at vibe coding, which I will henceforth call Ai assisted development. I tackled the same project again. I wanted to build an app that could detect all urls and javascript files on the page separately.</p>
<p>Before we do get into it, please subscribe to my blog.</p>
<p>For those who want to read my first attempt you can find it here:</p>
<p><a target="_blank" href="https://blog.v1shnu.com/my-first-attempt-at-vibe-coding">Vibe Coding Part 1</a></p>
<p>For a quick summary my two mistakes were:</p>
<ol>
<li><p>Writing short prompts</p>
</li>
<li><p>Not understanding the api and frameworks I was using, even at a basic level.</p>
</li>
</ol>
<p>I corrected mistake 2 by reading up on extensions api. I now know the difference between content and background scripts. I also understand how message passing works which will come later.</p>
<p>With the script that I had written earlier I found multiple issues, it only worked on the loaded urls, and it didn’t look forward at other urls on the page leading to being kind of useless to what I actually wanted to do.</p>
<p>So I went back to claude better armed, my prompt is below:</p>
<pre><code class="lang-plaintext">create a chrome extension which grabs all url hostnames and javascript urls, 
puts them into two seperate arrays and then logs them when page load is complete
</code></pre>
<p>My prompt had a much clearer description on exactly what I wanted to do, and Claude generated a ton of code for me and surprisingly (compared to my previous effort), I only had one error.</p>
<pre><code class="lang-plaintext">Why did this error occur, 
Uncaught SyntaxError: Identifier 'observer' has already been declared do not rewrite code
</code></pre>
<p>Claude actually solved it this time:</p>
<pre><code class="lang-plaintext">I see the issue. The error "Uncaught SyntaxError: Identifier 'observer' has already been 
declared" happens because we have both the content script automatically injected by the 
manifest (via the content_scripts entry) AND manually injected through the 
chrome.scripting.executeScript call in background.js. 

This creates a conflict where the content script runs twice, causing the 'observer' variable 
to be declared multiple times.Let's fix this by choosing one injection method. Since we have 
it defined in the manifest's content_scripts section, we should remove the manual injection 
from background.js:
</code></pre>
<p>Claude also generated extraneous code. The entire background.js is useless as claude did not pass any messages from content.js to it.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//content.js</span>
<span class="hljs-comment">// Initialize arrays to store the hostnames and JavaScript URLs</span>
hostnames = [];
jsUrls = [];

<span class="hljs-comment">// Function to extract hostname from a URL</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extractHostname</span>(<span class="hljs-params">url</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> URL(url, <span class="hljs-built_in">window</span>.location.href).hostname;
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
}

<span class="hljs-comment">// Function to collect all hostnames from various elements</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">collectHostnames</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Add current page hostname</span>
  <span class="hljs-keyword">const</span> currentHostname = <span class="hljs-built_in">window</span>.location.hostname;
  <span class="hljs-keyword">if</span> (currentHostname &amp;&amp; !hostnames.includes(currentHostname)) {
    hostnames.push(currentHostname);
  }

  <span class="hljs-comment">// Collect hostnames from all links</span>
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'a[href]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">link</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> hostname = extractHostname(link.href);
    <span class="hljs-keyword">if</span> (hostname &amp;&amp; !hostnames.includes(hostname)) {
      hostnames.push(hostname);
    }
  });

  <span class="hljs-comment">// Collect hostnames from images</span>
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'img[src]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">img</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> hostname = extractHostname(img.src);
    <span class="hljs-keyword">if</span> (hostname &amp;&amp; !hostnames.includes(hostname)) {
      hostnames.push(hostname);
    }
  });

  <span class="hljs-comment">// Collect hostnames from iframes</span>
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'iframe[src]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">iframe</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> hostname = extractHostname(iframe.src);
    <span class="hljs-keyword">if</span> (hostname &amp;&amp; !hostnames.includes(hostname)) {
      hostnames.push(hostname);
    }
  });

  <span class="hljs-comment">// Collect hostnames from sources</span>
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'source[src]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">source</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> hostname = extractHostname(source.src);
    <span class="hljs-keyword">if</span> (hostname &amp;&amp; !hostnames.includes(hostname)) {
      hostnames.push(hostname);
    }
  });

  <span class="hljs-comment">// Collect hostnames from audio/video elements</span>
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'audio[src], video[src]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">media</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> hostname = extractHostname(media.src);
    <span class="hljs-keyword">if</span> (hostname &amp;&amp; !hostnames.includes(hostname)) {
      hostnames.push(hostname);
    }
  });
}

<span class="hljs-comment">// Function to collect all JavaScript URLs</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">collectJsUrls</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'script[src]'</span>).forEach(<span class="hljs-function"><span class="hljs-params">script</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> url = script.src;
    <span class="hljs-keyword">if</span> (url &amp;&amp; !jsUrls.includes(url)) {
      jsUrls.push(url);
    }
  });
}

<span class="hljs-comment">// Function to log the collected data</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logData</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'--- URL and JS Logger Extension ---'</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Hostnames collected:'</span>, hostnames);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'JavaScript URLs collected:'</span>, jsUrls);

  <span class="hljs-comment">// Send data to background script</span>
  chrome.runtime.sendMessage({
    <span class="hljs-attr">action</span>: <span class="hljs-string">'urlDataCollected'</span>,
    <span class="hljs-attr">data</span>: {
      <span class="hljs-attr">hostnames</span>: hostnames,
      <span class="hljs-attr">jsUrls</span>: jsUrls
    }
  });
}

<span class="hljs-comment">// Wait for the page to fully load</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'load'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// Give a short delay to ensure dynamic content has loaded</span>
  <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
    collectHostnames();
    collectJsUrls();
    logData();
  }, <span class="hljs-number">1000</span>);
});

<span class="hljs-comment">// Observe DOM changes to catch dynamically added content</span>
<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-function">(<span class="hljs-params">mutations</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> shouldUpdate = <span class="hljs-literal">false</span>;

  mutations.forEach(<span class="hljs-function"><span class="hljs-params">mutation</span> =&gt;</span> {
    <span class="hljs-keyword">if</span> (mutation.addedNodes.length) {
      shouldUpdate = <span class="hljs-literal">true</span>;
    }
  });

  <span class="hljs-keyword">if</span> (shouldUpdate) {
    collectHostnames();
    collectJsUrls();
  }
});

<span class="hljs-comment">// Start observing the document</span>
observer.observe(<span class="hljs-built_in">document</span>.documentElement, {
  <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span>
});

<span class="hljs-comment">// Stop observing after 10 seconds to prevent continuous updates</span>
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
  observer.disconnect();
  collectHostnames();
  collectJsUrls();
  logData();
}, <span class="hljs-number">10000</span>);
</code></pre>
<p>My favourite part of all this is it runs. As a proof of concept it works much better than what I had built and Claude’s first attempt. This actually demonstrates that vibe coding is actually useful!</p>
]]></content:encoded></item><item><title><![CDATA[My First Attempt at Vibe Coding]]></title><description><![CDATA[Introduction
If you’ve been on LinkedIn or Reddit there has been a huge push for vibe coding. It talks about people building entire applications without any programming expertise, solving problems quickly, rapid iteration, and so on and so forth. It ...]]></description><link>https://blog.v1shnu.com/my-first-attempt-at-vibe-coding</link><guid isPermaLink="true">https://blog.v1shnu.com/my-first-attempt-at-vibe-coding</guid><category><![CDATA[vibe coding]]></category><category><![CDATA[vibes]]></category><category><![CDATA[chrome extension]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Tue, 29 Apr 2025 06:16:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/6UDansS-rPI/upload/998b857ba8f7596bb09f8fc57855d16a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction">Introduction</h3>
<p>If you’ve been on LinkedIn or Reddit there has been a huge push for vibe coding. It talks about people building entire applications without any programming expertise, solving problems quickly, rapid iteration, and so on and so forth. It sounds awesome, I am not a professional coder by any means, but I work in the tech industry and I have a lot of ideas. So it sounded perfect.</p>
<h3 id="heading-what-i-wanted-to-build">What I wanted to Build</h3>
<p>What I wanted to build was simple, I just wanted something that would pull all urls that where loaded on a page, whether it was initial load or by js in the background. Now for this to happen I needed to run in the browser but outside the webpage. The best option is something Google hates, Extensions. I also have zero experience developing extensions, but I do know enough JS to be dangerous.</p>
<p>This was the opportunity I had been waiting for. Now I’m not going to pay for anything, so no integrated IDE environments, meaning no cursor or copilot. I read through all the AI rankings, (I didn’t really) I just remembered that everyone was hyping Claude, so thats what I tried with.</p>
<p>The first prompt I used was:</p>
<pre><code class="lang-plaintext">how do I detect a url call on a page
</code></pre>
<p>Its a pretty poor prompt, but interestingly Claude actually understood me intent (Yes using “understood” is wrong when speaking about LLMs but it highlights how deterministic language is). The output had 4 different options:</p>
<ol>
<li><p>Using the Fetch API or XMLHttpRequest Event Listeners</p>
</li>
<li><p>Using the Performance API</p>
</li>
<li><p>Using Browser DevTools Network Monitor</p>
</li>
<li><p>Using Service Workers</p>
</li>
</ol>
<p>All of these are good ideas, and I would not have thought of options 2 and 3. However none of these work for me because I can’t load JS on the page as I want to monitor pages I don’t own. So I reframed my question.</p>
<pre><code class="lang-plaintext">how about using an extension
</code></pre>
<p>I will skip all of the ai fluff agreeing with the suggestion. It again gave me three options.</p>
<ol>
<li><p>Use an existing module - for my purposes useless.</p>
</li>
<li><p>Extension using a service worker</p>
</li>
<li><p>Extension using a content injection model.</p>
</li>
</ol>
<p>So whats the difference between a service worker extension and a content injection extension? I didn’t know at the time. To be honest I am not clear on it now either. It is something that I need to look into when I further develop this extension that I am building. So going with the theme of this I went with the vibe and I chose option 2. To see where it would take me. This is the code it gave me.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"URL Call Detector"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0"</span>,
  <span class="hljs-attr">"manifest_version"</span>: <span class="hljs-number">3</span>,
  <span class="hljs-attr">"permissions"</span>: [<span class="hljs-string">"webRequest"</span>, <span class="hljs-string">"webRequestBlocking"</span>, <span class="hljs-string">"&lt;all_urls&gt;"</span>],
  <span class="hljs-attr">"background"</span>: {
    <span class="hljs-attr">"service_worker"</span>: <span class="hljs-string">"background.js"</span>
  }
}
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// background.js</span>
chrome.webRequest.onBeforeRequest.addListener(
  <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">details</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"URL call detected:"</span>, details.url);
    <span class="hljs-comment">// You can also log method, type, timestamp, etc.</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-attr">cancel</span>: <span class="hljs-literal">false</span>}; <span class="hljs-comment">// Don't block the request</span>
  },
  {<span class="hljs-attr">urls</span>: [<span class="hljs-string">"&lt;all_urls&gt;"</span>]},
  [<span class="hljs-string">"blocking"</span>]
);
</code></pre>
<p>This code is remarkably close to what I wanted to achieve, in fact it looked perfect. I pulled it altogether into chrome and tried to run it but it failed with errors. Now I have some background context on why it was failing. Google had recently (read June 2024) deprecated Manifest v2. Now I wouldn’t really know much about this except for the fact that this broke every adblocker out there. The replacement module Manifest v3 is a poor replacement as it doesn’t allow the same level of access to block ads.</p>
<p>Why did I suspect that this was the issue, well, the manifest version is set to 3, and we have the “blocking tag.” I ended up going to the docs and yep that was the issue. In a blue box at the top of the page with a star</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745905472342/97116792-4849-4842-a412-e40f0fa71451.png" alt class="image--center mx-auto" /></p>
<p>So I went back to Claude because this is an experiment in vibe coding.</p>
<pre><code class="lang-plaintext">request blocking does not work with manifest3
</code></pre>
<p>It decided to start from scratch and decided to give me this code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// background.js</span>
<span class="hljs-comment">// For monitoring requests without blocking them</span>
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(
  <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Matched URL:"</span>, info.request.url);
    <span class="hljs-comment">// Store the request info if needed</span>
    chrome.storage.local.get([<span class="hljs-string">'requests'</span>], <span class="hljs-function">(<span class="hljs-params">result</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> requests = result.requests || [];
      requests.push({
        <span class="hljs-attr">url</span>: info.request.url,
        <span class="hljs-attr">method</span>: info.request.method,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now()
      });
      chrome.storage.local.set({<span class="hljs-attr">requests</span>: requests});
    });
  }
);

<span class="hljs-comment">// Set up rules to match all requests</span>
chrome.declarativeNetRequest.updateSessionRules({
  <span class="hljs-attr">addRules</span>: [{
    <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">priority</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">action</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"allow"</span> },
    <span class="hljs-attr">condition</span>: {
      <span class="hljs-attr">urlFilter</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">resourceTypes</span>: [<span class="hljs-string">"main_frame"</span>, <span class="hljs-string">"sub_frame"</span>, <span class="hljs-string">"stylesheet"</span>, <span class="hljs-string">"script"</span>, <span class="hljs-string">"image"</span>, <span class="hljs-string">"font"</span>, <span class="hljs-string">"object"</span>, <span class="hljs-string">"xmlhttprequest"</span>, <span class="hljs-string">"ping"</span>, <span class="hljs-string">"other"</span>]
    }
  }]
});
</code></pre>
<p>This did not work either, I went back to Claude with the error that I found.</p>
<pre><code class="lang-plaintext">Trying to use option 1 leads to the error 
Uncaught TypeError: Cannot read properties of undefined (reading 'addListener')
</code></pre>
<p>In response Clade decided to start again. It generated new code that did not work either. To cut a long story short I kept going back with errors and it kept coming back with solutions that did not work. After a an hour or two of this bullshit (Note my frustration at this point). What was worse was that it was generating even more complex code, rather than continuing to solve my problem. It generated a popup.htm file, and even more things.</p>
<p>In the end out of sheer frustration I deleted everything and went back to the original prompt response and went to the original docs. This is what I ended up with.</p>
<pre><code class="lang-javascript">chrome.webRequest.onBeforeRequest.addListener(
    <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">details</span>) </span>{
      <span class="hljs-built_in">console</span>.log(details.urls);
    },
    {<span class="hljs-attr">urls</span>: [<span class="hljs-string">"&lt;all_urls&gt;"</span>]},
  );
</code></pre>
<p>It worked!</p>
<p>So what where the fundamental errors? There were actually 2, the first was Claude generating more and more complex code without understanding what I was trying to do. Also I was feeding it bad info because of the second issue. When you run a background service the errors or the log output does not appear in the default console. Instead there is a seperate console that you have to open a seperate console. This means that there was a really good chance (read definite) that I was feeding Claude the wrong error messages as well.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>You have to remember there are claims that people are deploying entire applications after building it with vibe coding with no programming experience. My experience demonstrates that these claims are highly suspect. A person with no programming experience is not going to be able to utilise AI in a productive manner. We need to have a basic understanding and also need to read docs to be able to utilise this correctly. What I did find is that if I need to generate basic code quickly, this is super useful. I generated a website and it looked great. After I understood how to test properly the generated code actually makes sense.</p>
]]></content:encoded></item><item><title><![CDATA[Designing a Budgeting App]]></title><description><![CDATA[In the last article we spoke about why you need a financial application. We dived into the features required and why we’d build one. In this article I will share with you how I designed Yafa. This will focus on the backend choices and how I designed ...]]></description><link>https://blog.v1shnu.com/designing-a-budgeting-app</link><guid isPermaLink="true">https://blog.v1shnu.com/designing-a-budgeting-app</guid><category><![CDATA[yafa]]></category><category><![CDATA[budget]]></category><category><![CDATA[Python]]></category><category><![CDATA[Django]]></category><category><![CDATA[apps]]></category><category><![CDATA[DIY]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Mon, 21 Apr 2025 00:33:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2JIvboGLeho/upload/a61f598bf6532cc4d5e5fcb6fda1340c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://blog.v1shnu.com/why-you-need-a-financial-applicationand-why-i-built-my-own">last article</a> we spoke about why you need a financial application. We dived into the features required and why we’d build one. In this article I will share with you how I designed Yafa. This will focus on the backend choices and how I designed the database models as well.</p>
<p>Before we get started lets quickly go over the features that we absolutely need in the application.</p>
<ol>
<li><p>A budgeting application needs to keep track of transactions. It needs to check for expenses and income.</p>
</li>
<li><p>These transactions need labels of some kind. How do we know if income is from salary or is it from rent. This is why we should be able to categorise transactions. Transactions can belong to multiple categories as well. Consequently we should have categories.</p>
</li>
<li><p>Now that we know how money is flowing we need to track where its going to. We have accounts for this purpose. We have bank accounts which store money and where want to track expenses to. We also have expense accounts, eg. loan accounts and credit cards. In these cases do we want to track the transactions or do we want to track payments. For a budgeting app we want to see credit card transactions, but we want to track payments as expenses as we only pay when we pay the credit card.</p>
</li>
<li><p>Now that we have an underlying idea of the financial side of the application, we need to decide on the more generic features of the application. We need user models, groups, so we can have multiple budgets etc.</p>
</li>
</ol>
<p>Before we can commit to backend frameworks and database models and the like we need to understand the flow if data. This will help shape both the UI and also the server calls and database models that need to be built.</p>
<p>The actions the user can take are:</p>
<ol>
<li><p>User creates account.</p>
</li>
<li><p>User logs in.</p>
</li>
<li><p>User requests support</p>
</li>
<li><p>User updates settings.</p>
</li>
<li><p>User creates account.</p>
</li>
<li><p>User creates category.</p>
</li>
<li><p>User creates transaction.</p>
</li>
</ol>
<p>There are only 7 actions that need to be implemented for the Minimum Viable Product and only 3 that are part of the “finance” portion of the application.</p>
<h2 id="heading-choosing-a-framework">Choosing a Framework</h2>
<p>Now that we have described the app and the operations of the app, lets look at what framework to use. Modern day development uses react on top of nextjs or other JS framework that spans both front end and back end. The problem for me is that I want to deploy quickly and I don’t have time to learn a framework. I have a pretty strong background in python and also raw JS. Further to this my sql is intermediate at best if I were to be generous.</p>
<p>For me this means choosing a python web framework and the two that I have used in the past are Django and Flask. I have no interest in implementing users or building everything from scratch. Django does a lot of things automatically and its batteries included method is what I want to use. It provides an administration page to manage all the database models, as well as user models that can be used for our own user management. Further by using groups we can solve the groups issue as well.</p>
<h2 id="heading-database-amp-models">Database &amp; Models</h2>
<p>Django also allows us to build database models, that can be deployed on pretty much any relational db engine. For production postgres is the obvious choice. Its open source well supported and scales relatively easily. We know that Django implements models directly in the framework itself. This actually allows us to abstract away many to many relationships as well, making model development quick and easy.</p>
<h3 id="heading-category-model">Category Model</h3>
<p>The category model is the simplest of all the models. All we need is name and a description. There is nothing else required right at this moment.</p>
<h3 id="heading-account-model">Account Model</h3>
<p>The account model should contain a name and description like the categories. It should also contain the type of account. Is it an expense account like a credit card or is it a normal account like a bank account. Finally it should contain a balance figure. By having a balance number it avoids us having to pull every single transaction from all history to calculate the balance.</p>
<h3 id="heading-transactions-model">Transactions Model</h3>
<p>The transaction model is the most complex. It requires:</p>
<ul>
<li><p>The transaction date</p>
</li>
<li><p>A name</p>
</li>
<li><p>A description</p>
</li>
<li><p>A foreign key to the account to which the transaction is.</p>
</li>
<li><p>A foreign key to the user who created it</p>
</li>
<li><p>A Many to Many relationship with all the categories to which its associated. (A category can be associated with many transactions, and a transaction can be associated with many categories).</p>
</li>
<li><p>The amount of the transaction and if its an income or expense. In my case I specified that this should be two seperate fields so a transaction could contain both income and expense at the sam time. Further by doing this we can avoid signed numbers as well. Rather we can use the most basic type an unsigned int to represent value.</p>
<ul>
<li><p>With money calculations we need to use the integer or big integer fields to store or validate date. The reason for this is quite simple, floats are inaccurate and poor representations of money. See <a target="_blank" href="https://news.ycombinator.com/item?id=27677683">here</a> for a longer discussion of the topic.</p>
</li>
<li><p>For our purposes storing in cents makes the most sense as it doesn’t need that much precision.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We’ll leave it here for today, over the next set of articles we’ll look at building out the application on one thread and look at designing the fundamental features of the budgeting application. Sign up to my newsletter so that you can follow along.</p>
]]></content:encoded></item><item><title><![CDATA[Why You Need a Financial Application—and Why I Built my own]]></title><description><![CDATA[Financial decisions today are increasingly complex and in a world where every dollar counts, keeping track of your money is more important than ever. Budgeting applications are essential for managing money, monitor spending, set financial goals, and ...]]></description><link>https://blog.v1shnu.com/why-you-need-a-financial-applicationand-why-i-built-my-own</link><guid isPermaLink="true">https://blog.v1shnu.com/why-you-need-a-financial-applicationand-why-i-built-my-own</guid><category><![CDATA[yafa]]></category><category><![CDATA[budget]]></category><category><![CDATA[Problem statement]]></category><category><![CDATA[Build In Public]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Fri, 18 Apr 2025 04:33:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/djb1whucfBY/upload/f0f028f30db98c18805d8152083eea5b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Financial decisions today are increasingly complex and in a world where every dollar counts, keeping track of your money is more important than ever. Budgeting applications are essential for managing money, monitor spending, set financial goals, and ultimately build a better relationship with money. But with many apps on the market, each with its quirks, subscriptions, and limitations, how do we choose?</p>
<p>Let’s explore why budgeting apps are important and why I chose to build my own.</p>
<hr />
<h3 id="heading-why-you-need-a-budgeting-app">Why You Need a Budgeting App</h3>
<p><strong>1. Gain Control Over Your Finances</strong><br />Most people including me had a vague idea of their financial situation (bad) until they start tracking it. Budgeting apps should provide clarity by showing you exactly where your money goes. Whether you're trying to pay off debt, save for a big purchase, or stop living pay check to pay check, tracking your finances is the first step.</p>
<p><strong>2. Set and Track Goals</strong><br />Want to save $200k for a down payment? Cut your food spending by $20? A budgeting app makes goal-setting tangible, allowing you to break your big dreams into manageable steps and monitor your progress over time.</p>
<p><strong>3. Eliminate Mindless Spending</strong><br />Seeing daily, weekly, and monthly spending patterns in real time helps cut back on impulse buys. Once you recognise patterns (e.g. too much takeout, unused subscriptions), you can change behaviour and reallocate funds toward things that truly matter.</p>
<p><strong>4. Automate and Simplify</strong><br />Manually tracking expenses in a notebook or spreadsheet can work but it painful and confusing quickly. Apps automate data entry, categorise expenses, and may even pull in transactions from your bank or credit card, making it easier to keep up with your budget.</p>
<hr />
<h3 id="heading-why-you-should-build-your-own-budgeting-app">Why You Should Build Your Own Budgeting App</h3>
<p>While apps like YNAB, Frollo, and EveryDollar are popular for a reason, they’re not without downsides. Building your own budgeting application gives you total control and offers several benefits that off the shelf apps just can’t match.</p>
<p><strong>1. You Can Build the Features <em>You</em> Need</strong><br />Most budgeting apps try to serve a broad audience. As a result, they often include features you’ll never use or leave out the ones you desperately need. Maybe you need envelope style budgeting but with crypto integration. Or maybe you want a visual calendar view of your cash flow. When you build your own app, you build what you need.</p>
<p>With Yafa I wanted a couple of features in particular. I wanted unlimited accounts, unlimited categories, take actions based on categories and the like. This was not something available to me with the other apps.</p>
<p><strong>2. Say Goodbye to Monthly Fees</strong><br />Many top-tier budgeting tools come with subscription fees ranging from $5 to $15 a month. That might not seem like much, but it adds up—especially when the whole point is to <em>save</em> money. Building your own app is a one-time investment of time and (possibly) some initial setup cost, but it can save hundreds over the years.</p>
<p>Yafa is built to be free. I have minimised all my costs for running the application. Its sits behind Cloudflare, it runs on a cloud provider, where costs are amortised over the multiple services I run. To me the costs of running an application are minimal to non-existant. The true cost is my time and for various reasons deploying Yafa is well worth the trouble.</p>
<p><strong>3. Learn Valuable Skills</strong><br />Even if you’re not a professional developer, creating your own budgeting app is a great way to learn programming, data structures, API integration, and UX design. It’s a practical project with real-world value.</p>
<p><strong>4. Integrate With Your Life Seamlessly</strong><br />Want to pull data from your custom bank API? Sync it with your calendar? Add voice command capabilities? Building your own app means you’re only limited by your creativity. You can integrate your budgeting tool into your digital ecosystem in ways no third-party app ever could.</p>
<p>I don’t want to sync with my bank accounts, but I want the ability to search my data and make connections via search. By having access to the database I can do this. By building it as a feature I can give anyone thsi power.</p>
<hr />
<h3 id="heading-what-youll-need-to-get-started">What You’ll Need to Get Started</h3>
<ul>
<li><p><strong>Tech Stack</strong>: Depending on your skills, you can use a web-based stack (React, Node.js, Firebase), a spreadsheet-powered backend (Google Sheets + Apps Script), or even a mobile-first approach (Flutter, Swift).</p>
</li>
<li><p><strong>Budgeting Features</strong>: Think categories, recurring expenses, goal tracking, graphs, and reporting.</p>
</li>
<li><p><strong>Security</strong>: Implement strong encryption and consider offline storage or local hosting to ensure your data stays secure.</p>
</li>
<li><p><strong>UI/UX</strong>: Clean design is crucial for daily usability—don’t skip on making it intuitive and visually appealing.</p>
</li>
</ul>
<p>Over the coming weeks I will get into a technical deep dive of how I built Yafa, how to use it, its quirks, its features and ultimately why I think you should use it.</p>
<hr />
<h3 id="heading-final-thoughts">Final Thoughts</h3>
<p>Using a budgeting app is one of the smartest ways to stay in control of your finances. Off-the-shelf solutions can work for many, they often come with compromises. If you’re looking for a tailored, cost-effective, and private solution, yafa may be the solution for you.</p>
<p>Over the course of this series, I will go through features, design choices and how we put privacy first to build a solution first for myself and for you. Jump on board and not only will you gain a deeper understanding of your finances, but you'll also build something that grows with you and that might just be the best investment you make all year.</p>
<p>Check Yafa out here: <a target="_blank" href="https://yafa.v1shnu.com">yafa.v1shnu.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Better Multi-Select with Bootstrap]]></title><description><![CDATA[The select component is one of the most fundamentally frustrating components in HTML forms. This is what it looks like:

The code needed to collect the values from this object is also complicated. For most inputs you can get the value by calling .val...]]></description><link>https://blog.v1shnu.com/building-a-better-multi-select-with-bootstrap</link><guid isPermaLink="true">https://blog.v1shnu.com/building-a-better-multi-select-with-bootstrap</guid><category><![CDATA[HTML5]]></category><category><![CDATA[Bootstrap 5]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[select]]></category><category><![CDATA[Multiselect]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Thu, 17 Apr 2025 07:21:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/vw3Ahg4x1tY/upload/784584d55212ea1ab0d31227b693f289.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The select component is one of the most fundamentally frustrating components in HTML forms. This is what it looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744871522022/4e3d583a-0aad-464a-9625-138a273d7698.png" alt="Select with multiple" class="image--center mx-auto" /></p>
<p>The code needed to collect the values from this object is also complicated. For most inputs you can get the value by calling .value. In this case you have to write your own code to get the value.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> category = <span class="hljs-built_in">Array</span>.from(category_select.options).filter(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">option</span>) </span>{
    <span class="hljs-keyword">return</span> option.selected;
}).map(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">option</span>) </span>{
    <span class="hljs-keyword">return</span> option.value;
});
</code></pre>
<h3 id="heading-styled-multi-select">Styled Multi Select</h3>
<p>Our solution needs to have two key features, the first is that it should be much better looking and second it should be easy to get the value. This is why I built the styled Multi Select which quickly shows all selected values, with a stylised drop down that matches the bootstrap style.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/Cyber-and-me/pen/zxxYMZR">https://codepen.io/Cyber-and-me/pen/zxxYMZR</a></div>
<p> </p>
<p>To get the value, you simply call the value function.</p>
<pre><code class="lang-javascript">sms.value
</code></pre>
<p>Conclusion:</p>
<p>I have used this extensively. To get a hold of this element you can get it from my github.</p>
<p><a target="_blank" href="https://github.com/VishnuUnnikrishnan/HTMLComponents.git">Github</a></p>
]]></content:encoded></item><item><title><![CDATA[Terms and Conditions]]></title><description><![CDATA[v1shnu.com is a portfolio of works. This is used to demonstrate various projects and proof of concepts.
The products and services offered on this domain are offered as is and there is no guarantee of any sort. Usage of these services does not imply a...]]></description><link>https://blog.v1shnu.com/terms-and-conditions</link><guid isPermaLink="true">https://blog.v1shnu.com/terms-and-conditions</guid><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Sat, 25 May 2024 09:04:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/EX9QVVFtQxw/upload/50cf2bb18b90ee125b56ab66c130d50f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>v1shnu.com is a portfolio of works. This is used to demonstrate various projects and proof of concepts.</p>
<p>The products and services offered on this domain are offered as is and there is no guarantee of any sort. Usage of these services does not imply any contract of any sort unless otherwise guaranteed by law.</p>
<p>If you have any concerns please reach out to hello@v1shnu.com.</p>
<p>Any personal information we gather will be treated as per our privacy policy.</p>
]]></content:encoded></item><item><title><![CDATA[Build a better Digital Photo Frame]]></title><description><![CDATA[TL;DR
Building a Digital Photo Frame is important proof of concept in demonstrating how open-source technologies can be utilized to build a product that can not only compete with but also exceed what’s available in the market.

Our Aims:
Digital phot...]]></description><link>https://blog.v1shnu.com/build-a-better-digital-photo-frame</link><guid isPermaLink="true">https://blog.v1shnu.com/build-a-better-digital-photo-frame</guid><category><![CDATA[eink]]></category><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[product]]></category><category><![CDATA[buildingandlearning]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Wed, 03 Jan 2024 02:18:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/wQLAGv4_OYs/upload/fe068f7cf1bdaa75ac6bab438ac4fe0c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR</h1>
<p>Building a Digital Photo Frame is important proof of concept in demonstrating how open-source technologies can be utilized to build a product that can not only compete with but also exceed what’s available in the market.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704247706204/647d10d5-6e59-42e6-84f5-08d8829fa9d5.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-our-aims">Our Aims:</h1>
<p>Digital photo frames are a solved problem. You can buy as many as you want at all price ranges, from less than a $100 to over $600. Beyond the obvious as a learning exercise, why do we do this. There are multiple problems with existing photo frames.</p>
<p>These are:</p>
<ol>
<li><p>The vast, vast majority of digital photo frames have LCD panels. This means that to show an image they need to be powered, making them in my opinion vastly inferior to an existing photo frame. Worse still LCD panels have viewing angles which means you have to stand in one spot to see the image perfectly.</p>
</li>
<li><p>Commercial photo frames often use old buggy and old versions of software and run on top of old and buggy hardware. This means that the experience of using the photo frame is terrible. They are slow buggy and entirely useless. Uploading photos and getting it setup is an entirely difficult process.</p>
</li>
<li><p>Commercial photo frames never update their software and often run modified versions of existing commercial software. Furthermore, they often connect to Wi-Fi and the internet. This results in insecure devices connecting to the internet and increases the chances of security breaches at home, from theft of personal photos on device through to ransomware and involvement of your network on cyberattacks.</p>
</li>
<li><p>Finally commercial hardware is locked down and built by factories with three or four factors of separation from the end user. This means that if anything breaks down, there is no way to get it fixed. The device is never getting repaired and even if the retailer replaces it, the device will end up in landfill as another piece of e-waste.</p>
</li>
</ol>
<p>My aim is quite simple. I want to address the issues that we have seen above. That is simply it. Point 1 was unsolvable even a few years ago. We have a solution now and it is E-Ink. Yes, the same technology that runs the Kindle and known for being black and white is our solution.</p>
<p>Modern E-Ink can display up to 7 colours which is more than enough to cover the colour space when we apply various intelligent algorithms.  Further to this, E-Ink is like paper, it stays in the set state when the power is turned off and it doesn’t emit light, rather it reflects it. This combined together means that we don’t have an angles problem and we don’t have a power problem.</p>
<p>Challenges two to four can be resolved far more quickly by using opensource or well supported technologies. In our case we are using the Raspberry Pi 0w for this proof of concept. This device is well supported and was first released in 2017.  More than 6 years later it is still being sold and with the operating system based on Linux, it will continue to receive updates and continually be supported. This means that our device could have individual parts upgraded thereby preventing e-waste.</p>
<h1 id="heading-build-guide">Build Guide</h1>
<p>There are two parts to this build guide. The physical product and the software component. Both are important to ensure the success of the product. The Physical component affects the look and feel of the device, and we need to make sure that any design looks as good or better than what is available. The software component determines how smooth the operation of using it is. For the purposes of this version, we are going to have a simple rotating display with uploads of photos via ssh. In future we will implement a web server for the Raspberry Pi that can be used to connect to Wi-Fi as well as upload images.</p>
<h2 id="heading-physical-build-guide">Physical Build Guide</h2>
<p>Building the device is the last part of the process. However, it has the most considerations as it is the first thing that people see and feel. There are many approaches we could have taken, and I chose a traditional look. There are multiple approaches we could have taken; we could have gone completely the opposite way and created a post-modern piece, or we could have built something that showed off the technological aspects.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704247362093/3d291c09-0914-42f1-9917-071554832341.jpeg" alt class="image--center mx-auto" /></p>
<p>As mentioned, we chose the traditional route. There is a reason photo frames have been unchanged for centuries. There is a frame, a glass front and so on. In fact I just bought a relatively cheap frame from a local retailer. With a few modifications I was able to fit the E-Ink screen into a traditional frame. The Raspberry Pi 0w is a tiny and slim device which can be attached to the back quite easily. Even as a prototype the device looks quite good.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704247389438/087efabd-cbb1-4292-8638-828609a43114.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-parts-list">Parts List</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Parts</td><td>AUD ($)</td><td>USD ($)</td></tr>
</thead>
<tbody>
<tr>
<td>6-inch Photo Frame</td><td>6</td><td>4</td></tr>
<tr>
<td>5.65-inch ACeP 7-Color E-Paper E-Ink Display Module</td><td>92</td><td>62.99</td></tr>
<tr>
<td>Raspberry Pi Zero W</td><td>24.50</td><td>15</td></tr>
<tr>
<td>Total (parts)</td><td>122.50</td><td>81.99</td></tr>
</tbody>
</table>
</div><h2 id="heading-software-build-guide">Software Build Guide</h2>
<p>The software build guide is straightforward as well, we have utilized the Waveshare libraries which allow us to minimize the software that needs to be written. What we have done is a script that updates the screen with the picture from a folder that the converter programs update with new photos. We also update a file to keep track of the photo that is being displayed.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> waveshare_epd <span class="hljs-keyword">import</span> epd5in65f
<span class="hljs-keyword">from</span> PIL <span class="hljs-keyword">import</span> Image
<span class="hljs-keyword">from</span> os <span class="hljs-keyword">import</span> listdir

<span class="hljs-keyword">try</span>:
    imagefiles = [f <span class="hljs-keyword">for</span> f <span class="hljs-keyword">in</span> listdir(<span class="hljs-string">"photos"</span>)]
    print(imagefiles)

    <span class="hljs-keyword">with</span> open(<span class="hljs-string">"currimage.txt"</span>,<span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
        line = f.readline()
        displayImage = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> line:
            index = imagefiles.index(line)
            <span class="hljs-keyword">if</span> index == len(imagefiles)<span class="hljs-number">-1</span>:
                index = <span class="hljs-number">0</span>
            <span class="hljs-keyword">else</span>:
                index = index + <span class="hljs-number">1</span>
            displayImage = imagefiles[index]
        <span class="hljs-keyword">else</span>:
            displayImage = imagefiles[<span class="hljs-number">0</span>]

    <span class="hljs-keyword">with</span> open(<span class="hljs-string">"currimage.txt"</span>,<span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> f:
        f.write(displayImage)

    <span class="hljs-comment">#Clear and set the WaveShare Display</span>
    epd = epd5in65f.EPD()
    epd.init()
    epd.Clear()

    Himage = Image.open(<span class="hljs-string">"photos/"</span>+displayImage)
    epd.display(epd.getbuffer(Himage))
    epd5in65f.epdconfig.module_exit()
</code></pre>
<p>We then set up a bash script to be run very hour via a cron job.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment">#Change to the right directory update this to point </span>
<span class="hljs-built_in">cd</span> /home/pi/PhotoFrame
<span class="hljs-comment">#Run Script</span>
python photoframe.py
</code></pre>
<pre><code class="lang-bash"><span class="hljs-comment"># m h  dom mon dow   command</span>
0 * * * * /home/pi/PhotoFrame/photoframe.sh
</code></pre>
<p>Download all code and follow installation guidelines from <a target="_blank" href="https://github.com/VishnuUnnikrishnan/photoframe">GitHub</a></p>
<h1 id="heading-future-work">Future Work</h1>
<p>This is a prototype to prove that this is possible, however there are various improvements that can be made.</p>
<ol>
<li><p>Currently the only way to upload photos is via ssh. This is fine for a single one-off solution, however if this were to be turned into a product, we would need to have another method to upload photos.</p>
</li>
<li><p>There is no way to change wifi connections or set up in hotspot mode. This will need to be available for people to set up their device.</p>
</li>
<li><p>Finally, this is a prototype, a proper product would need to have all the parts integrated in a single circuit board. This would make the product thinner, and we would not see any exposed cables as we do with the prototype.</p>
</li>
<li><p>The last major change would be moving away from the fully fledged computer in the Pi to a microcontroller like the Pico. This would reduce the overall cost of the product.</p>
</li>
</ol>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Consumer electronics are rarely profitable, especially in commoditized spaces like digital photo frames. Even from a cost analysis, this device would cost around $200 per unit to build. This puts it already out of reach of most people. To be profitable we would have to sell it for $400 - $500 which is at the high end of the digital photo frame market.  Changes in the design and volume discounts would bring the price down.</p>
<p>While this may put a dampener on the idea of sales, it achieves our other aims. To encourage people to build their own products, and beyond that it has highlighted that we can build a better product that what is available on the market. This ultimately achieved what we set out to do.</p>
<p>Have ideas on what I should build next? Put it into the comments.</p>
]]></content:encoded></item><item><title><![CDATA[E-Ink Photo Frame - Part 1]]></title><description><![CDATA[TL;DR:
This post briefly looks at what is required to create an image file that can be displayed on the Waveshare 5.65inch ACeP 7-Color E-Paper E-Ink Display Module. It goes into the required format and links to a script that converts images into the...]]></description><link>https://blog.v1shnu.com/e-ink-photo-frame-part-1</link><guid isPermaLink="true">https://blog.v1shnu.com/e-ink-photo-frame-part-1</guid><category><![CDATA[eink]]></category><category><![CDATA[Python]]></category><category><![CDATA[pillow]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Mon, 01 Jan 2024 06:35:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/54VAb3f1z6w/upload/54035aa4a25d366c874bbe7cb2ef5f72.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR:</h1>
<p>This post briefly looks at what is required to create an image file that can be displayed on the Waveshare 5.65inch ACeP 7-Color E-Paper E-Ink Display Module. It goes into the required format and links to a script that converts images into the correct format, which is available on <a target="_blank" href="https://github.com/VishnuUnnikrishnan/photoframe/blob/main/converter.py">Github</a>.</p>
<h1 id="heading-converting-images">Converting Images:</h1>
<p>There are a couple of challenges with utilizing the Waveshare libraries. The image must be the exact size of the display and they must be in the right 7 colour format. Finally, it needs to be in bmp format. Otherwise, the code crashes or in the best case it won’t be displayed correctly on the platform.</p>
<h1 id="heading-format-conversion">Format Conversion:</h1>
<p>Format conversion is pretty straightforward, you can utilize various tools to convert a file to bmp. The script that has been provided will do this automatically.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">toBMP</span>(<span class="hljs-params">input</span>):</span>
    image = Image.open(input).convert(<span class="hljs-string">'RGB'</span>)
    <span class="hljs-keyword">return</span> image
</code></pre>
<h1 id="heading-resizing">Resizing:</h1>
<p>For the “5.65inch ACeP 7-Color E-Paper E-Ink Display Module” which I’ve used the size is 600x448 pixels. This size is different on different screens so make sure that you check the size before beginning to resize the image.</p>
<p>The naïve way to resize an image is to crop it. However, if you crop a modern photo with millions of pixels, you’re going to end up with a tiny corner of an image. To solve this issue, we need to first scale the image down to have a length or height of 600px depending on whether you have a landscape of portrait image. Following that we need to crop the corresponding height or length to 448px.</p>
<p>Again we face an issue, we cannot just crop the height, it may be that after the resizing it may be less than the expected 448px and so we need to pad the image to make it the exact size.</p>
<p>Further to this we should not just crop or pad on the top or bottom, we need to divide it in two and apply it to both the top and bottom. This will minimise the total data loss in the image.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cutToSize</span>(<span class="hljs-params">image, outLength, outHeight</span>):</span>

    length, height = image.size

    <span class="hljs-keyword">if</span> length &lt; height:
        image = image.rotate(<span class="hljs-number">90</span>, expand=<span class="hljs-number">1</span>)

    length, height = image.size

    ratio = length/outLength
    tempHeight = height/ratio

    image = image.resize((outLength, math.floor(tempHeight)))

    difference = tempHeight - outHeight

    partVal = abs(difference)/<span class="hljs-number">2</span>

    bot = math.ceil(partVal)
    top = math.floor(partVal)

    <span class="hljs-keyword">if</span> difference &gt; <span class="hljs-number">0</span>:
        image = image.crop((<span class="hljs-number">0</span>, top, outLength, tempHeight-top))
        image = image.rotate(<span class="hljs-number">180</span>, expand=<span class="hljs-number">1</span>)

    <span class="hljs-keyword">elif</span> difference &lt; <span class="hljs-number">0</span>:
        tempImage = Image.new(image.mode, (outLength, outHeight), (<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>))
        tempImage.paste(image, (<span class="hljs-number">0</span>, top))
        image = tempImage

    <span class="hljs-keyword">return</span> image
</code></pre>
<h1 id="heading-changing-the-colors">Changing the Colors:</h1>
<p>The next step is changing the colors. The E-ink display I used only supports 7 colors. The palette can be seen below. This is pretty awesome as for the longest time E-ink only supported black and white. However, it does mean we have to change the image’s colour palette from 256 colours (for BMP files) to 7.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704090475909/3f2cf66b-bd03-42e2-8cf3-a5fa6d40d297.png" alt class="image--center mx-auto" /></p>
<p>The simplest approach is to find the closest matching color and apply it.</p>
<p>This would look strange, and we would lose a lot of detail. This raises the question, is there a better way?</p>
<p>Yes, there is, and this method is called dithering. Dithering is where randomized noise is introduced to reduce banding as we saw in the previous image. Banding is the solid bands of colours that you saw in the example above. Dithering also creates the illusion of colour depth. It does this by taking advantage of the human eye which perceives a collection of colours as a mixture of these colours. This allows 7 colours to create a much bigger colour space. This allows us to get high quality pictures on the screen without having to sacrifice to much detail in the images.</p>
<p>Applying this in code is not straightforward, the documentation is poor and the what I have built is from combining multiple stack overflow answers and various gists.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">palette7Convert</span>(<span class="hljs-params">image</span>):</span>
    palettedata = [<span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>,
                   <span class="hljs-number">0xff</span>, <span class="hljs-number">0xff</span>, <span class="hljs-number">0xff</span>,
                   <span class="hljs-number">0x00</span>, <span class="hljs-number">0xff</span>, <span class="hljs-number">0x00</span>,
                   <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0xff</span>,
                   <span class="hljs-number">0xff</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>,
                   <span class="hljs-number">0xff</span>, <span class="hljs-number">0xff</span>, <span class="hljs-number">0x00</span>,
                   <span class="hljs-number">0xff</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>,
                  ]


    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, <span class="hljs-number">249</span> * <span class="hljs-number">3</span>):
        palettedata.append(<span class="hljs-number">0</span>)

    p_img = Image.new(<span class="hljs-string">'P'</span>, (<span class="hljs-number">600</span>, <span class="hljs-number">448</span>))
    p_img.putpalette(palettedata)

    image = image.quantize(palette=p_img, dither=<span class="hljs-number">1</span>)

    <span class="hljs-keyword">return</span> image
</code></pre>
<h1 id="heading-conclusion">Conclusion**:**</h1>
<p>Converting an image to be displayed on the 7 colour E ink display is more involved than originally expected. The script allows us to avoid the manual work of creating each image independently thereby saving us time and effort.</p>
]]></content:encoded></item><item><title><![CDATA[Privacy Policy]]></title><description><![CDATA[V1shnu.com was built as a portfolio of works.

At v1shnu.com, accessible from v1shnu.com, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by v1shnu.c...]]></description><link>https://blog.v1shnu.com/privacy-policy</link><guid isPermaLink="true">https://blog.v1shnu.com/privacy-policy</guid><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Sat, 04 Nov 2023 04:35:18 GMT</pubDate><content:encoded><![CDATA[<p>V1shnu.com was built as a portfolio of works.</p>

<p>At v1shnu.com, accessible from v1shnu.com, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by v1shnu.com and how we use it.</p>

<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us.</p>

<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in v1shnu.com. This policy is not applicable to any information collected offline or via channels other than this website.</p>

<h2>Consent</h2>

<p>By using our website, you hereby consent to our Privacy Policy and agree to its terms.</p>

<h2>Information we collect</h2>

<p>The personal information that you are asked to provide, and the reasons why you are asked to provide it, will be made clear to you at the point we ask you to provide your personal information.</p>
<p>If you contact us directly, we may receive additional information about you such as your name, email address, phone number, the contents of the message and/or attachments you may send us, and any other information you may choose to provide.</p>
<p>When you register for an Account, we may ask for your contact information, including items such as name, company name, address, email address, and telephone number.</p>

<h2>How we use your information</h2>

<p>We use the information we collect in various ways, including to:</p>

<ul>
<li>Provide, operate, and maintain our website</li>
<li>Improve, personalize, and expand our website</li>
<li>Understand and analyze how you use our website</li>
<li>Develop new products, services, features, and functionality</li>
<li>Communicate with you, either directly or through one of our partners, including for customer service, to provide you with updates and other information relating to the website, and for marketing and promotional purposes</li>
<li>Send you emails</li>
<li>Find and prevent fraud</li>
</ul>

<h2>Log Files</h2>

<p>v1shnu.com follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>

<h2>Cookies and Web Beacons</h2>

<p>Like any other website, v1shnu.com uses "cookies". These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>

<h2>Google DoubleClick DART Cookie</h2>

<p>Google is one of a third-party vendor on our site. It also uses cookies, known as DART cookies, to serve ads to our site visitors based upon their visit to www.website.com and other sites on the internet. However, visitors may choose to decline the use of DART cookies by visiting the Google ad and content network Privacy Policy at the following URL – <a href="https://policies.google.com/technologies/ads">https://policies.google.com/technologies/ads</a></p>

<h2>Advertising Partners Privacy Policies</h2>

<p></p><p>You may consult this list to find the Privacy Policy for each of the advertising partners of v1shnu.com.</p><p></p>
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on v1shnu.com, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>

<p>Note that v1shnu.com has no access to or control over these cookies that are used by third-party advertisers.</p>

<h2>Third Party Privacy Policies</h2>

<p>v1shnu.com's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. </p>

<p>You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites.</p>

<h2>CCPA Privacy Rights (Do Not Sell My Personal Information)</h2>

<p>Under the CCPA, among other rights, California consumers have the right to:</p>
<p>Request that a business that collects a consumer's personal data disclose the categories and specific pieces of personal data that a business has collected about consumers.</p>
<p>Request that a business delete any personal data about the consumer that a business has collected.</p>
<p>Request that a business that sells a consumer's personal data, not sell the consumer's personal data.</p>
<p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.</p>

<h2>GDPR Data Protection Rights</h2>

<p>We would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:</p>
<p>The right to access – You have the right to request copies of your personal data. We may charge you a small fee for this service.</p>
<p>The right to rectification – You have the right to request that we correct any information you believe is inaccurate. You also have the right to request that we complete the information you believe is incomplete.</p>
<p>The right to erasure – You have the right to request that we erase your personal data, under certain conditions.</p>
<p>The right to restrict processing – You have the right to request that we restrict the processing of your personal data, under certain conditions.</p>
<p>The right to object to processing – You have the right to object to our processing of your personal data, under certain conditions.</p>
<p>The right to data portability – You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under certain conditions.</p>
<p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.</p>

<h2>Children's Information</h2>

<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p>

<p>v1shnu.com does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>

<h2>Changes to This Privacy Policy</h2>

<p>We may update our Privacy Policy from time to time. Thus, we advise you to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately, after they are posted on this page.</p>

<h2>Contact Us</h2>

<p>If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.</p>]]></content:encoded></item><item><title><![CDATA[Mapping the Stars]]></title><description><![CDATA[Night Sky - AI Summary:
This article delves into the creation of personalized celestial maps using the D3-Celestial library, catering to the trend of commemorating special occasions with starry mementos. The author simplifies the process through clie...]]></description><link>https://blog.v1shnu.com/mapping-the-stars</link><guid isPermaLink="true">https://blog.v1shnu.com/mapping-the-stars</guid><category><![CDATA[D3.js]]></category><category><![CDATA[sky]]></category><category><![CDATA[stars]]></category><category><![CDATA[d3-celestial.js]]></category><dc:creator><![CDATA[Vishnu]]></dc:creator><pubDate>Thu, 26 Oct 2023 07:55:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/9wH624ALFQA/upload/8c5adeed4d56312cc8b5b8f98bb8dee2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-night-sky-ai-summary">Night Sky - AI Summary:</h2>
<p>This article delves into the creation of personalized celestial maps using the D3-Celestial library, catering to the trend of commemorating special occasions with starry mementos. The author simplifies the process through client-side development and dynamic data input. Challenges and solutions, such as constellation name issues and browser compatibility, are discussed, and the project successfully delivers a functional and accurate star map.</p>
<p>Check it out at <a target="_blank" href="https://nightsky.v1shnu.com">Night Sky</a></p>
<h2 id="heading-introduction">Introduction:</h2>
<p>There is an enduring trend where you can get a printout of the night sky of your special occasion to keep as a memento or give away. It is a thoughtful and fun gift to give that makes a lot of sense, and for me I’ve always had a love of space, and astronomy. The problem is if you google for something like this, you will get a bunch of choices and all of them look pretty similar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698304454778/63430fbb-d53f-404d-a0bc-61be82106f8c.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698304500983/484d702d-05e8-420d-a35c-33acbbc2fdff.png" alt class="image--center mx-auto" /></p>
<p>When I see something like this I start to wonder, is this as difficult as I think it might be? The answer was no, not at all. You can even do it all client side.</p>
<h2 id="heading-building-night-sky">Building Night Sky:</h2>
<p>The first thing I needed to do was to get all of the data. Five minutes on Google let me find an even better solution, which was d3-celestial. This was a better solution because it integrates data collection, processing, and display all in one. For those not in the know, D3.js is a well-known library used for visualizing data. What you can do with it is pretty epic. Many of the stunning visual graphics on the internet are generated by the creative use of D3.js.</p>
<p>D3-Celestial provides us with a Celestial map with all of the features that every other paid service has. Looking at the way certain sites work, I am 100% sure they use this library. The great thing about this library is there is almost no code to write all you have to do is modify a JSON configuration.</p>
<pre><code class="lang-json">var config = { 
    width: <span class="hljs-number">0</span>,           <span class="hljs-comment">// Default width, 0 = full parent element width; </span>
                       <span class="hljs-comment">// height is determined by projection</span>
    projection: <span class="hljs-string">"airy"</span>,    <span class="hljs-comment">// Map projection used: see below</span>
    projectionRatio: <span class="hljs-literal">null</span>,   <span class="hljs-comment">// Optional override for default projection ratio</span>
    transform: <span class="hljs-string">"equatorial"</span>, <span class="hljs-comment">// Coordinate transformation: equatorial (default),</span>
                             <span class="hljs-comment">// ecliptic, galactic, supergalactic</span>
    center: <span class="hljs-literal">null</span>,       <span class="hljs-comment">// Initial center coordinates in set transform</span>
                        <span class="hljs-comment">// [longitude, latitude, orientation] all in degrees </span>
                        <span class="hljs-comment">// null = default center [0,0,0]</span>
    orientationfixed: <span class="hljs-literal">true</span>,  <span class="hljs-comment">// Keep orientation angle the same as center[2]</span>
    geopos: <span class="hljs-literal">null</span>,       <span class="hljs-comment">// optional initial geographic position [lat,lon] in degrees, </span>
                        <span class="hljs-comment">// overrides center</span>
    ...
</code></pre>
<p>The library is optimized for use with npm, however, we can still use it directly, which is what I did. This is simpler and faster.</p>
<p>The only information we need to collect from the user is the latitude, longitude, and time at this location. We need this information as these three factors determine which direction you are facing. As the Earth spins, the stars and constellations move. Once we feed this data in, it generates a star chart with all of the info we require, including stars, constellations, deep space objects, the sun, moon and planets and their positions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698305252530/2d4c3764-6278-43a0-bbe4-a48636aaed0b.png" alt class="image--center mx-auto" /></p>
<p>The json config also gives us the option to add in lines, modify projection type, change background colour and make a whole host of changes to make the image look pretty. This is what it looks like by default on my version.</p>
<h2 id="heading-launching-night-sky"><strong>Launching Night Sky:</strong></h2>
<p>The next step was to launch this. From what I could see some players in this space generate the maps server side while others do it client side or don’t display the map at all. Instead, they exist purely to sell a physical product. The applications use a major cloud provider like Google or run applications on Vercel which has a generous free tier. The way that I wrote Night Sky means that I can deploy my application as a static application. I could deploy it in an s3 bucket, on Github pages or even a cheap hosting provider.</p>
<p>I chose to do it via Cloudflare. I already owned a domain name through them and most importantly, it is free to host a static website via Cloudflare pages. The advantage over Github pages is that we can have many projects with their own domain names. I also wanted to understand Cloudflare’s feature set, so it was a double win for me.</p>
<p>Launching on Cloudflare was easy, all you had to do was link to your Github repository. Any updates to the repository are automatically collected and deployed by Cloudflare. Having only ever deployed webpages in the days of Wordpress, having an automated pipeline is amazing.</p>
<h2 id="heading-night-sky-challenges">Night Sky Challenges</h2>
<p>No writeup is complete without highlighting the challenges that have occurred. This was a relatively simple project and the D3-Celestial library is well built. However, there have been a couple of issues. The first was constellation names showing up as undefined, and the second was canvas size being too large on non-Chromium based browsers.</p>
<p>In the example config the constellation name set is set to “iau” however in the dataset there is no “iau” dataset. Instead, you have to use the “name” dataset. It caused a weird bug where on initial load with constellations enabled it would correctly load, however, any subsequent load would see constellation names load as undefined.</p>
<p>The non-Chromium browser issue was fixed by adding in a single line into the css.</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">canvas</span>{
    <span class="hljs-attribute">max-width</span>: <span class="hljs-number">100%</span> <span class="hljs-meta">!important</span>;
}
</code></pre>
<p>This bug affected Safari, Firefox and all iOS browsers which is a huge part of modern web traffic.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This was a simple project but it achieved what it set out to do, build a working, accurate star map. Check it out at <a target="_blank" href="https://nightsky.v1shnu.com">Night Sky</a>.</p>
]]></content:encoded></item></channel></rss>