<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>vim abozanona</title>
  
  
  <link href="https://abozanona.me/atom.xml" rel="self"/>
  
  <link href="https://abozanona.me/"/>
  <updated>2026-06-06T23:47:18.532Z</updated>
  <id>https://abozanona.me/</id>
  
  <author>
    <name>Abozanona</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Israeli forces have shot and killed a 7-month-old baby in my neighbourhood</title>
    <link href="https://abozanona.me/israeli-forces-have-shot-and-killed-a-7-month-old-baby-in-my-neighbourhood/"/>
    <id>https://abozanona.me/israeli-forces-have-shot-and-killed-a-7-month-old-baby-in-my-neighbourhood/</id>
    <published>2026-06-06T21:23:23.000Z</published>
    <updated>2026-06-06T23:47:18.532Z</updated>
    
    <content type="html"><![CDATA[<p>I was on the phone yesterday when my mom told me how close she was to being shot and killed by Israeli soldiers near our home.</p><span id="more"></span><blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/p/DZN_bRDj9PL/" data-instgrm-version="14" style="max-height: 615px;"></blockquote><script async src="https://www.instagram.com/embed.js"></script><p>She was returning home from a relative’s wedding. My brother was driving her, along with his wife and my other brother’s wife. Then the car ran out of diesel.</p><p>This is the story my mom told me.</p><blockquote><p>We were on our way home. Your brother was driving down the hill near the Shahin family’s house when his car ran out of diesel fuel. He had to pull over and park on the side of the road. I and your brothers’ wives decided to continue home on foot since the house was nearby.</p><p>As we were walking downhill, we were surprised to encounter a group of Israeli occupation soldiers on patrol in front of us. The soldiers told us to turn around. But I replied, “Turn around and go where? Our house is right there. I want to go home!”</p><p>The soldiers repeated, “Turn around and go back.” I kept shouting at them, saying, “Our house is there!” Your brothers’ wives told me, “Let’s wait. We don’t want the soldiers to get angry and do something.” So we waited on the sidewalk until they allowed us to pass.</p><p>After some time, the soldiers moved up to the top of the hill and let us continue. We walked down while the soldiers headed upward.</p><p>When the soldiers reached the top, one of them turned toward a car that had been parked, waiting for the patrol to pass, and calmly opened fire on the people inside while they were still sitting in the vehicle.</p><p>After the soldiers left, people gathered around the car to see what happened and to help the injured. I also went over to look. They found a 7-month-old baby who had been shot. The bullet entered through his mouth and lodged in his mother’s jaw. The boy’s father had also been shot in the hand.</p></blockquote><blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/reel/DZQW9f_IBbE/" data-instgrm-version="14" style="max-height: 615px;"></blockquote><p>My family were steps away from death. If not for God’s protection, and if the car had not run out of diesel at that exact moment, it could have been my family in that car.</p><p>Indeed we belong to Allah, and indeed to Him we shall return. May Allah strengthen the hearts of the child’s family and grant them patience, and may He take retribution from the Israeli army that caused this loss.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;I was on the phone yesterday when my mom told me how close she was to being shot and killed by Israeli soldiers near our home.&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>Despite her pregnancy, Palestinian refugee reveals details of Dutch police assault on his wife.</title>
    <link href="https://abozanona.me/palestinian-refugee-reveals-details-of-dutch-police-assault-on-his-wife/"/>
    <id>https://abozanona.me/palestinian-refugee-reveals-details-of-dutch-police-assault-on-his-wife/</id>
    <published>2026-06-05T21:23:23.000Z</published>
    <updated>2026-06-07T00:21:41.382Z</updated>
    
    <content type="html"><![CDATA[<p>A video of a Dutch policeman throwing a heavily pregnant woman to the ground has caused outrage. The woman says police attacked her at a migration centre where authorities had detained her Palestinian husband.</p><span id="more"></span><video controls preload="metadata" playsinline style="width: 100%; max-height: 615px;">  <source src="https://elec.abozanona.me/palestinian-refugee-reveals-details-of-dutch-police-assault-on-his-wife.mp4" type="video/mp4">  <track    src="https://abozanona.me/assets/subtitles/palestinian-refugee-reveals-details-of-dutch-police-assault-on-his-wife.vtt"    kind="subtitles"    srclang="en"    label="English"    default  >  Your browser does not support the video tag.</video><p>Watch original assult video here</p><iframe height="250" src="https://www.youtube.com/embed/vMKpAb5R1Ds" style="width:100%;"></iframe>]]></content>
    
    
    <summary type="html">&lt;p&gt;A video of a Dutch policeman throwing a heavily pregnant woman to the ground has caused outrage. The woman says police attacked her at a migration centre where authorities had detained her Palestinian husband.&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>Pacman Graph is now Arcade Graph</title>
    <link href="https://abozanona.me/pacman-graph-is-now-arcade-graph/"/>
    <id>https://abozanona.me/pacman-graph-is-now-arcade-graph/</id>
    <published>2026-05-08T21:23:23.000Z</published>
    <updated>2026-06-06T04:15:25.201Z</updated>
    
    <content type="html"><![CDATA[<p>Big update: <strong>Pacman Contribution Graph</strong> has evolved into <strong>Arcade Contribution Graph</strong>.</p><p>What started as one Pacman game is now a growing arcade of contribution-graph games for GitHub and GitLab.</p><span id="more"></span><h2 id="From-One-Game-to-an-Arcade"><a href="#From-One-Game-to-an-Arcade" class="headerlink" title="From One Game to an Arcade"></a>From One Game to an Arcade</h2><p>Pacman Contribution Graph began with one goal: make contribution graphs more fun and alive.</p><p>Now the project supports multiple arcade experiences:</p><ul><li><strong>Pac-Man</strong></li><li><strong>Breakout</strong></li><li><strong>Galaga</strong></li><li><strong>Puzzle Bobble</strong></li><li><strong>Bomberman</strong></li><li><strong>Minesweeper</strong></li></ul><p>And this is only the beginning.</p><p>Project repo: <a href="https://github.com/abozanona/pacman-contribution-graph">github.com&#x2F;abozanona&#x2F;pacman-contribution-graph</a></p><h2 id="Thank-You-to-All-Contributors"><a href="#Thank-You-to-All-Contributors" class="headerlink" title="Thank You to All Contributors"></a>Thank You to All Contributors</h2><p>This transition from Pacman to Arcade Graph would not be possible without everyone who contributed code, ideas, fixes, and improvements.</p><p>Huge thanks to:</p><ul><li><a href="https://github.com/AndreRuperto">@AndreRuperto</a></li><li><a href="https://github.com/Serveny">@Serveny</a></li><li><a href="https://github.com/Hiro-Chiba">@Hiro-Chiba</a></li><li><a href="https://github.com/nexpectArpit">@nexpectArpit</a></li><li><a href="https://github.com/VagnerNerves">@VagnerNerves</a></li></ul><p>You helped shape both the original Pacman game and the new arcade direction.</p><h2 id="10-000-Users-and-a-Live-Leaderboard"><a href="#10-000-Users-and-a-Live-Leaderboard" class="headerlink" title="10,000+ Users and a Live Leaderboard"></a>10,000+ Users and a Live Leaderboard</h2><p>The project now has <strong>over 10,000 GitHub users</strong> who integrated Pacman&#x2F;Arcade contribution games into their accounts.</p><p>You can check the leaderboard here:</p><p><a href="https://abozanona.github.io/pacman-contribution-graph/leaderboard.html">Arcade Contribution Graph Leaderboard</a></p><h2 id="If-You-Haven’t-Integrated-It-Yet-Try-It"><a href="#If-You-Haven’t-Integrated-It-Yet-Try-It" class="headerlink" title="If You Haven’t Integrated It Yet, Try It"></a>If You Haven’t Integrated It Yet, Try It</h2><p>If you still have not added it to your profile, now is the perfect time. It is super easy to set up with GitHub Actions, and you can generate one or multiple games.</p><p>Start here:</p><ul><li>Live demo: <a href="https://abozanona.github.io/pacman-contribution-graph/">abozanona.github.io&#x2F;pacman-contribution-graph</a></li><li>Repository: <a href="https://github.com/abozanona/pacman-contribution-graph">github.com&#x2F;abozanona&#x2F;pacman-contribution-graph</a></li></ul><p>Let your contribution graph play games.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Big update: &lt;strong&gt;Pacman Contribution Graph&lt;/strong&gt; has evolved into &lt;strong&gt;Arcade Contribution Graph&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What started as one Pacman game is now a growing arcade of contribution-graph games for GitHub and GitLab.&lt;/p&gt;</summary>
    
    
    
    <category term="project" scheme="https://abozanona.me/categories/project/"/>
    
    
  </entry>
  
  <entry>
    <title>How to Remotely Debug a Flutter App on an iPhone Over the Internet</title>
    <link href="https://abozanona.me/how-to-remotely-debug-a-flutter-app-on-an-iphone-over-the-internet/"/>
    <id>https://abozanona.me/how-to-remotely-debug-a-flutter-app-on-an-iphone-over-the-internet/</id>
    <published>2026-02-13T22:23:23.000Z</published>
    <updated>2026-06-06T23:15:59.464Z</updated>
    
    <content type="html"><![CDATA[<p>I was working on a Flutter project for a friend in Jordan when we hit an issue that I could not reproduce on my side. Logs were not enough, and I needed to debug the app directly on his iPhone.</p><span id="more"></span><p>The main problem was that iPhone debugging is normally straightforward only when the phone is physically connected to your Mac, and in practice the setup is built around devices being on the same local environment. Since my friend was in another country, I had to recreate that setup over the internet.</p><h2 id="The-Goal"><a href="#The-Goal" class="headerlink" title="The Goal"></a>The Goal</h2><p>I needed three things:</p><ol><li>A way to put my Mac and my friend’s laptop on the same private network.</li><li>A way to make his physically connected iPhone appear as a USB device on my Mac.</li><li>A way to make Xcode and Flutter recognize that remote iPhone so I could run and debug the app.</li></ol><p>The tools I used were:</p><ul><li><a href="https://tailscale.com/">Tailscale</a> for the network layer.</li><li><a href="https://www.virtualhere.com/usb_server_software">VirtualHere USB Server</a> on my friend’s side.</li><li><a href="https://www.virtualhere.com/usb_client_software">VirtualHere USB Client</a> on my side.</li><li>Xcode on both Macs.</li></ul><h2 id="Step-1-Put-Both-Machines-on-the-Same-Network-With-Tailscale"><a href="#Step-1-Put-Both-Machines-on-the-Same-Network-With-Tailscale" class="headerlink" title="Step 1: Put Both Machines on the Same Network With Tailscale"></a>Step 1: Put Both Machines on the Same Network With Tailscale</h2><p>The first challenge was networking. I needed my Mac and my friend’s laptop to reach each other privately over the internet as if they were on the same trusted network.</p><p>That is where <a href="https://tailscale.com/">Tailscale</a> helped. It creates a private mesh VPN between devices and is very easy to set up. For this use case, that was exactly what I needed.</p><p>Another nice detail is that Tailscale has a free personal plan, which is enough for connecting your own devices or a small number of machines for this kind of debugging session.</p><p>Once Tailscale was installed and both Macs were connected to the same tailnet, the network problem was basically solved.</p><h2 id="Step-2-Share-the-iPhone-USB-Connection-Over-the-Internet"><a href="#Step-2-Share-the-iPhone-USB-Connection-Over-the-Internet" class="headerlink" title="Step 2: Share the iPhone USB Connection Over the Internet"></a>Step 2: Share the iPhone USB Connection Over the Internet</h2><p>Being on the same network was not enough on its own. The iPhone was still physically connected to my friend’s laptop, not mine.</p><p>To solve that, I used <a href="https://www.virtualhere.com/">VirtualHere</a>.</p><p>The setup is simple:</p><ul><li>Install the <strong>server</strong> on the machine that has the iPhone physically connected to it. In my case, that was my friend’s laptop.</li><li>Install the <strong>client</strong> on your own machine. In my case, that was my Mac.</li></ul><p>After that, the iPhone can be exposed from the remote laptop to your local Mac as if it were plugged in locally.</p><p>This was the missing piece. Tailscale handled the secure network path, and VirtualHere handled the USB forwarding.</p><h2 id="Step-3-Enable-Developer-Mode-on-the-iPhone"><a href="#Step-3-Enable-Developer-Mode-on-the-iPhone" class="headerlink" title="Step 3: Enable Developer Mode on the iPhone"></a>Step 3: Enable Developer Mode on the iPhone</h2><p>Before trying to run anything from Xcode or Flutter, make sure the target iPhone has <strong>Developer Mode</strong> enabled.</p><p>Without that, the device may appear but you still will not be able to properly deploy and debug a development build.</p><p>Also make sure the usual trust prompts are accepted:</p><ul><li>Trust the computer on the iPhone.</li><li>Trust the iPhone from the Mac that is physically connected to it.</li></ul><h2 id="Step-4-Install-Xcode-on-Both-Macs"><a href="#Step-4-Install-Xcode-on-Both-Macs" class="headerlink" title="Step 4: Install Xcode on Both Macs"></a>Step 4: Install Xcode on Both Macs</h2><p>This part is important.</p><p>I needed Xcode on:</p><ul><li>my Mac, where I wanted to run Flutter and attach the debugger</li><li>my friend’s laptop, because that is the machine physically connected to the iPhone for the first-time setup and pairing steps</li></ul><p>When an iPhone is connected to Xcode for the first time, Xcode may spend a long time preparing the device and downloading or generating the required support files. Doing that remotely over the internet can be painfully slow.</p><p>What worked better for me was this:</p><ol><li>Open Xcode on my friend’s laptop while the iPhone is connected there.</li><li>Let Xcode finish the initial device preparation on his side.</li><li>Copy the generated device support files from his machine.</li><li>Send them to me.</li><li>Extract them into the same location on my Mac.</li></ol><p>The directory I needed was:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/Library/Developer/Xcode/iOS DeviceSupport/</span><br></pre></td></tr></table></figure><p>Compressing the relevant folder inside that directory and sending it over was much faster than waiting for my Mac to regenerate everything remotely.</p><p>It is also a good idea to keep the Xcode version aligned on both Macs to reduce compatibility issues.</p><h2 id="Step-5-Connect-to-the-Device-From-Your-Mac"><a href="#Step-5-Connect-to-the-Device-From-Your-Mac" class="headerlink" title="Step 5: Connect to the Device From Your Mac"></a>Step 5: Connect to the Device From Your Mac</h2><p>At this point, the flow looked like this:</p><ol><li>The iPhone was connected by cable to my friend’s laptop.</li><li>My friend’s laptop was running the VirtualHere USB server.</li><li>My Mac was running the VirtualHere USB client.</li><li>Both Macs were connected through Tailscale.</li></ol><p>Once the VirtualHere client attached the remote iPhone on my Mac, Xcode and Flutter could see it.</p><h2 id="Step-6-Monitor-Installation-Progress-in-Xcode"><a href="#Step-6-Monitor-Installation-Progress-in-Xcode" class="headerlink" title="Step 6: Monitor Installation Progress in Xcode"></a>Step 6: Monitor Installation Progress in Xcode</h2><p>If app installation feels slow or stuck, you can trace progress directly from Xcode logs.</p><p>In recent Xcode versions, open:</p><p><code>Window &gt; Devices and Simulators</code></p><p>Then:</p><ol><li>Select the physical iPhone from the left panel.</li><li>Open the device console&#x2F;log view.</li><li>Watch installation-related logs in real time while deploying.</li></ol><p>You can see detailed progress messages like this:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-[IXAppInstallCoordinator _clientDelegate_didUpdateProgress:forPhase:overallProgress:]: &lt;IXInitiatingAppInstallCoordinator&lt;0x827014e00&gt; identity:[com.example.app/XXXXXXXX-516C-4EB6-A17F-XXXXXXXXXXXX/[system-defined]] uuid:XXXXXXXX-3A3D-44F7-9DB5-XXXXXXXXXXXX creator:InstallCoordination Proxy&gt;: Got did update progress delegate with percentComplete 51.000000 phase IXCoordinatorProgressPhaseLoading overallProgress 30.600000</span><br></pre></td></tr></table></figure><p>These logs are useful to confirm that installation is still moving forward (for example, loading, staging, or validating) instead of actually hanging.</p><p>To confirm that Flutter detected the device, I used:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter devices</span><br></pre></td></tr></table></figure><p>Then I ran the app on that specific device using its identifier:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter run -d &lt;device-id&gt;</span><br></pre></td></tr></table></figure><p>If you want the exact identifier first, <code>flutter devices</code> will print it for you.</p><h2 id="What-This-Setup-Actually-Solved"><a href="#What-This-Setup-Actually-Solved" class="headerlink" title="What This Setup Actually Solved"></a>What This Setup Actually Solved</h2><p>This setup gave me something much better than logs: a real debugging environment on the exact phone that had the issue.</p><p>That mattered because the bug was not reproducible anywhere else. Once I could run the app directly on my friend’s iPhone, I could inspect the behavior properly instead of guessing from screenshots and log messages.</p><h2 id="Final-Notes"><a href="#Final-Notes" class="headerlink" title="Final Notes"></a>Final Notes</h2><p>If you need to debug a Flutter app on a remote iPhone over the internet, the stack that worked for me was:</p><ul><li>Tailscale to connect both Macs privately.</li><li>VirtualHere to forward the iPhone’s USB connection.</li><li>Xcode on both sides for the initial device preparation.</li><li>Flutter commands on my Mac to run the app on the remote iPhone.</li></ul><p>It is not the simplest setup, but when the affected user is far away and the bug only happens on their real device, it can save a lot of time.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;I was working on a Flutter project for a friend in Jordan when we hit an issue that I could not reproduce on my side. Logs were not enough, and I needed to debug the app directly on his iPhone.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>Spine2d animation generator (Upcoming Project)</title>
    <link href="https://abozanona.me/spine2d-animation-generator/"/>
    <id>https://abozanona.me/spine2d-animation-generator/</id>
    <published>2025-11-30T22:23:23.000Z</published>
    <updated>2025-12-01T11:56:59.642Z</updated>
    
    <content type="html"><![CDATA[<p>For the past four months, I’ve been quietly working on a project I’m really excited to finally share: <strong>Spine2D Animation Generator</strong>.</p><p>This idea has been growing in my mind for a long time. I’ve always wanted to come up with a simple way to turn ideas into animated characters without spending days rigging, segmenting, and keyframing manually.</p><span id="more"></span><p>So I started building exactly that. A UI-powered tool that allows anyone to generate <strong>Spine2D-style animations using AI prompts.</strong></p><hr><h2 id="💡-What-is-Spine2D-Animation-Generator"><a href="#💡-What-is-Spine2D-Animation-Generator" class="headerlink" title="💡 What is Spine2D Animation Generator?"></a>💡 What is Spine2D Animation Generator?</h2><p>Imagine describing a character with words “clothing style, colors, vibe”, and the system generates the character image for you.</p><p>Then you ask it to segment the character into parts. It does that too.</p><p>Tell it to add a skeleton &amp; bone structure. It responds.</p><p>Ask it to animate “walk, jump, blink, dance”. It animates.</p><p>All <em>with prompts</em>.</p><p>No drawing skills required. No complex rigging workflows.<br>Just <strong>type → generate → animate</strong>.</p><p>This is the vision.</p><hr><h2 id="🛠-Current-Progress"><a href="#🛠-Current-Progress" class="headerlink" title="🛠 Current Progress"></a>🛠 Current Progress</h2><p>The tool is still in development. I’ve been working on it only in my free time, so progress has been slow, but steady. The UI is coming together nicely, and many core features are already taking shape.</p><p>You can watch a short preview of what I have so far here 👇</p><iframe height="250" src="https://www.youtube.com/embed/QqksHyqdRlg" style="width:100%;"></iframe>  <p>I’ve also put up a temporary landing page for it: <strong><a href="https://spine2d.com/">https://spine2d.com/</a></strong></p><p>It’s a placeholder for now, but one day soon it will become the home of the full product.</p><p>If everything goes well, I hope to have a usable version sometime early next year.</p><p><img src="/images/projects/spine2d-animation-generator/app.webp"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;For the past four months, I’ve been quietly working on a project I’m really excited to finally share: &lt;strong&gt;Spine2D Animation Generator&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This idea has been growing in my mind for a long time. I’ve always wanted to come up with a simple way to turn ideas into animated characters without spending days rigging, segmenting, and keyframing manually.&lt;/p&gt;</summary>
    
    
    
    <category term="project" scheme="https://abozanona.me/categories/project/"/>
    
    
  </entry>
  
  <entry>
    <title>Al-Taghriba Al-Filistinia (The Palestinian Exodus)</title>
    <link href="https://abozanona.me/al-taghriba-al-filistinia/"/>
    <id>https://abozanona.me/al-taghriba-al-filistinia/</id>
    <published>2025-10-20T21:23:23.000Z</published>
    <updated>2025-12-01T17:24:38.600Z</updated>
    
    <content type="html"><![CDATA[<p><strong>Al-Taghriba Al-Filistinia</strong> (<em>The Palestinian Exodus</em>) is one of the most powerful and poignant TV series ever created to document the Palestinian Nakba (Catastrophe) of 1948. The series tells the tragic story of the mass displacement of Palestinians during the establishment of the State of Israel, and it deeply delves into the conflicts with British forces, Jewish militias, and the Palestinian resistance movements during that period. Released in 2004, this historical drama has become a monumental piece of art that accurately portrays the pain, resistance, and displacement of the Palestinian people.</p><span id="more"></span><script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.0/dist/APlayer.min.js"></script><div id="player-al-taghriba" class="aplayer"></div><script>const ap3 = new APlayer({    element: document.getElementById('player-al-taghriba'),    mini: false,    autoplay: false,    lrcType: 3,    mutex: true,    audio: [{        name: 'The Palestinian Exodus',        artist: 'Opening credits',        url: '/images/projects/al-taghriba-al-filistinia/al-taghriba-ost.mp3',        cover: '/images/projects/al-taghriba-al-filistinia/movie-poster.webp',        lrc: '/images/projects/al-taghriba-al-filistinia/al-taghriba-ost.lrc',        theme: '#ebd0c2'    }]});</script><p>The series follows the story of a Palestinian family from a village in Palestine, as they experience the horrors of 1948, the events of Nakba, and the political struggles that led to the destruction of their community and homeland. The story masterfully intertwines the personal lives of the characters with the broader political upheaval of the time, showing the devastating impact of colonialism, foreign occupation, and the subsequent refugee crisis that still affects millions of Palestinians today.</p><a href="https://www.imdb.com/title/tt3109682/" target="_blank">    <img class="no-popup-img" loading="lazy" src="/images/projects/al-taghriba-al-filistinia/al-taghriba-al-filistinia-imdb.webp"/></a><p>At its core, <em>Al-Taghriba Al-Filistinia</em> focuses on the journey of one Palestinian family, specifically the hardships they face as they try to survive the chaos of war and escape from their burning village. The protagonist, Hassan, and his family are forced to flee their home and begin a dangerous journey across Palestine, facing not only military forces but also betrayal and violence from various factions. The story highlights the struggle of Palestinians during the late 1940s and shows the diverse range of experiences, from those who resisted the British and Zionist gangs, to those who were forced to take refuge in neighboring Arab countries.</p><p>This series brings to life the tension between the various factions in Palestine at the time, the British occupation forces, the Zionist gangs, and the Palestinian resistance. The series also vividly depicts the horrors of mass displacement and the dehumanization of a people who were uprooted from their lands.</p><p>As a Palestinian, the events depicted in <em>Al-Taghriba Al-Filistinia</em> resonate deeply with me. My grandfather and his family were displaced from their own village, <strong>The Gerat Almasmiyah</strong> (<em>AlMasmiyeh Alkabeereh</em>), which was located in the southern region of Palestine. Much like the characters in the series, they were victims of the same brutal events that tore Palestine apart during the Nakba. Watching the series takes me back to my own family’s history, and I can’t help but feel a deep connection to the struggle that continues to this day.</p><p><img src="/images/projects/al-taghriba-al-filistinia/refugees-in-galilee.webp"></p><p>What makes <em>Al-Taghriba Al-Filistinia</em> even more significant to me is the work of a dedicated Palestinian mother, Alaa Radwan, who started translating the series into English recently. Alaa, originally displaced from Gaza, now lives in Cairo, but her heart remains deeply rooted in her Palestinian identity and heritage. Through her Patreon project, Alaa has taken it upon herself to translate the entire series (31 episodes in total) to ensure that people around the world, especially non-Arabic speakers, can learn about the Palestinian experience.</p><p>Watching the translated episodes, a friend and I were both deeply moved by the portrayal of the conflict, the loss, and the unwavering resilience of the Palestinian people. Through Alaa’s translations, my friend was able to learn about the long-standing conflict with British forces, the rise of Zionist militias, and the tireless efforts of Palestinian resistance fighters. But most importantly, he learned about the human cost of this struggle, the people, their families, and their unwavering desire to return to their homeland.</p><p>If you haven’t already, I highly encourage you to watch <em>Al-Taghriba Al-Filistinia</em> on Alaa’s Patreon page. She has done an incredible job of making this pivotal piece of Palestinian history accessible to a wider audience.</p><p>By supporting Alaa on Patreon, you’re not just watching a series, you’re supporting a passionate Palestinian woman who is keeping the memory of the Nakba alive, and ensuring that future generations understand the roots of the ongoing conflict.</p><p>So, take the time to support Alaa Radwan’s incredible project on Patreon and witness the story of Palestine like never before. We owe it to ourselves, our ancestors, and our future generations to ensure that these stories are told and shared with the world.</p><p><strong><a href="https://www.patreon.com/c/Taghriba_Filistinia">Click here to watch Al-Taghriba Al-Filistinia on Alaa’s Patreon!</a></strong></p><a href="https://www.patreon.com/c/Taghriba_Filistinia/" target="__blank"><div class="image-container"><img class="no-popup-img" loading="lazy" src="/images/projects/al-taghriba-al-filistinia/movie-poster.webp"/></div></a><style>.image-container {  position: relative;}.image-container::before {  content: "";  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);  width: 0;  height: 0;  border-style: solid;  border-width: 20px 0 20px 40px;  border-color: transparent transparent transparent #000000;}</style>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;strong&gt;Al-Taghriba Al-Filistinia&lt;/strong&gt; (&lt;em&gt;The Palestinian Exodus&lt;/em&gt;) is one of the most powerful and poignant TV series ever created to document the Palestinian Nakba (Catastrophe) of 1948. The series tells the tragic story of the mass displacement of Palestinians during the establishment of the State of Israel, and it deeply delves into the conflicts with British forces, Jewish militias, and the Palestinian resistance movements during that period. Released in 2004, this historical drama has become a monumental piece of art that accurately portrays the pain, resistance, and displacement of the Palestinian people.&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>How to Set a Fixed Port When Debugging Your Flutter Application</title>
    <link href="https://abozanona.me/how-to-set-a-fixed-port-when-debugging-flutter-application-on-chrome/"/>
    <id>https://abozanona.me/how-to-set-a-fixed-port-when-debugging-flutter-application-on-chrome/</id>
    <published>2025-09-20T21:23:23.000Z</published>
    <updated>2025-11-30T00:48:45.978Z</updated>
    
    <content type="html"><![CDATA[<p>I’ve been working on a Flutter mobile application that connects to an API. At the beginning, I decided to run the app on Chrome instead of debugging directly on an Android or iOS device because it’s simply faster to do so.</p><p>But then I faced an annoying issue: whenever I launched the app by opening the <code>main.dart</code> file and clicking on the green debug button, the app would run on a <strong>different port</strong> each time. This caused <strong>CORS issues</strong> whenever my Dart app sent requests to the API, since the origin kept changing.</p><p>So I needed a way to always run the app on the same port.</p><span id="more"></span><hr><h2 id="First-Attempt-Running-With-flutter-run"><a href="#First-Attempt-Running-With-flutter-run" class="headerlink" title="First Attempt: Running With flutter run"></a>First Attempt: Running With <code>flutter run</code></h2><p>The easiest way I found was to run this command from the terminal:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter run -d chrome --web-port=51996</span><br></pre></td></tr></table></figure><p>This works, and the app does run on the fixed port <code>51996</code>.</p><p>But there’s a problem: if you run it this way, you <strong>can’t attach the debugger directly</strong>.</p><p>To fix that, I had to add a custom configuration to my <code>.vscode/launch.json</code> file:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;configurations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Flutter Web Attach&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="string">&quot;attach&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dart&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;deviceId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;chrome&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;webPort&quot;</span><span class="punctuation">:</span> <span class="number">51996</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Then, the flow became:</p><ol><li>Run the command from the terminal.</li><li>Open the Debug sidebar in VS Code.</li><li>Select the <strong>“Flutter Web Attach”</strong> configuration.</li><li>Start debugging to attach the debugger to the running app in Chrome on that fixed port.</li></ol><p>This works fine, but it wasn’t very convenient for me. I wanted something simpler.</p><hr><h2 id="Second-Attempt-Using-dart-flutterAdditionalArgs"><a href="#Second-Attempt-Using-dart-flutterAdditionalArgs" class="headerlink" title="Second Attempt: Using dart.flutterAdditionalArgs"></a>Second Attempt: Using <code>dart.flutterAdditionalArgs</code></h2><p>Next, I found that you can add this setting in your <code>.vscode/settings.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;dart.flutterAdditionalArgs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;--web-port=51996&quot;</span><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>This automatically adds the <code>--web-port</code> argument every time a Dart command is run.</p><p>At first, this looked perfect. But then I started getting an error each time I opened VS Code:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">The Flutter Daemon failed to start.</span><br></pre></td></tr></table></figure><p>The reason is that <code>dart.flutterAdditionalArgs</code> applies not only when you run your app, but also when <strong>any Dart process</strong> starts — including the <strong>Flutter Daemon</strong>. Since the daemon wasn’t expecting that argument, it failed to start.</p><hr><h2 id="The-Final-and-Best-Solution-dart-flutterRunAdditionalArgs"><a href="#The-Final-and-Best-Solution-dart-flutterRunAdditionalArgs" class="headerlink" title="The Final (and Best) Solution: dart.flutterRunAdditionalArgs"></a>The Final (and Best) Solution: <code>dart.flutterRunAdditionalArgs</code></h2><p>After some searching, I found a better setting that only applies when you actually run your application:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;dart.flutterRunAdditionalArgs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;--web-port=51996&quot;</span><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>This was exactly what I needed. With this setting:</p><ul><li>I don’t need to add custom launch configurations.</li><li>I can still use the green debug button in VS Code.</li><li>My app always runs on the same port.</li><li>No more CORS issues.</li><li>And no more Flutter Daemon errors.</li></ul><hr><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>If you’re running a Flutter web app in Chrome during development and want to avoid CORS issues by sticking to the same port, just add this to your <code>.vscode/settings.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;dart.flutterRunAdditionalArgs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;--web-port=51996&quot;</span><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>That’s it. Clean, simple, and you still get the debugger attached automatically.</p><hr><h2 id="Update-The-Native-Solution-Flutter-3-38"><a href="#Update-The-Native-Solution-Flutter-3-38" class="headerlink" title="Update: The Native Solution (Flutter 3.38+)"></a>Update: The Native Solution (Flutter 3.38+)</h2><p>Starting with Flutter 3.38, there is a much cleaner, native way to handle this that doesn’t rely on VS Code specific settings. You can now use a configuration file named <code>web_dev_config.yaml</code> in the root of your project. This allows you to define persistent server settings that work regardless of whether you are running from the terminal, VS Code, or Android Studio.</p><p>Here is how to set it up:</p><ul><li>Create a file named web_dev_config.yaml in your project root.</li><li>Add the following content:</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">host:</span> <span class="string">&quot;0.0.0.0&quot;</span> <span class="comment"># Optional</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">51996</span>     <span class="comment"># Your fixed port</span></span><br></pre></td></tr></table></figure><p>This method is superior because it also allows you to configure advanced features like HTTPS certificates, custom response headers, and URL rewrites (proxies), which can help you solve CORS issues without even needing a fixed port in some cases.</p><p>Check out the breakdown of these new features in this video:</p><iframe height="300" src="https://www.youtube.com/embed/-AuAZTyRelY" style="width:100%;"></iframe>]]></content>
    
    
    <summary type="html">&lt;p&gt;I’ve been working on a Flutter mobile application that connects to an API. At the beginning, I decided to run the app on Chrome instead of debugging directly on an Android or iOS device because it’s simply faster to do so.&lt;/p&gt;
&lt;p&gt;But then I faced an annoying issue: whenever I launched the app by opening the &lt;code&gt;main.dart&lt;/code&gt; file and clicking on the green debug button, the app would run on a &lt;strong&gt;different port&lt;/strong&gt; each time. This caused &lt;strong&gt;CORS issues&lt;/strong&gt; whenever my Dart app sent requests to the API, since the origin kept changing.&lt;/p&gt;
&lt;p&gt;So I needed a way to always run the app on the same port.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>(VIDEO) Mothers write their children names on their bodies in gaza</title>
    <link href="https://abozanona.me/mothers-write-their-children-names-on-their-bodies-in-gaza/"/>
    <id>https://abozanona.me/mothers-write-their-children-names-on-their-bodies-in-gaza/</id>
    <published>2025-06-03T21:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.731Z</updated>
    
    <content type="html"><![CDATA[<p>To help identify them after bombings… Mothers in Gaza write their children’s names on their bodies, fearing they may lose their identities beneath the rubble.</p><span id="more"></span><p>This clip is from the documentary “From Ground Zero” which you can watch in its entirety on Watermelon+ streaming platform. The entire segment on these children was incredibly heartbreaking. They made a stop-motion animation to tell their story of writing their names on their bodies and then erasing them because it gave them nightmares.</p><div style="display:flex;justify-content:center">    <video loop controls style="max-height:70vh">    <source src="https://elec.abozanona.me/mothers_write_their_children_names_on_their_bodies_in_gaza.mp4" type="video/mp4">    </video></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;To help identify them after bombings… Mothers in Gaza write their children’s names on their bodies, fearing they may lose their identities beneath the rubble.&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>The hypocrisy of the western goverenments</title>
    <link href="https://abozanona.me/the-hypocrisy-of-the-west/"/>
    <id>https://abozanona.me/the-hypocrisy-of-the-west/</id>
    <published>2025-05-21T21:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.733Z</updated>
    
    <content type="html"><![CDATA[<div></div><span id="more"></span><p><strong>29 November 2024</strong><br>After more than 13 months of Israeli attacks on Gaza, at least 44,382 people have been killed. Over 105,000 have been wounded, and thousands more are still missing or under the rubble. Despite these horrifying numbers, countries like the United States, Germany, and the United Kingdom continue to send weapons to Israel—helping them carry out more destruction in Gaza and more violence in the West Bank.</p><p>Western governments keep saying that “Israel has the right to defend itself.” They repeat this over and over, even while entire families are being wiped out. They don’t call for a stop to the violence. They just ask Israel to “be more careful” when killing innocent people.</p><p><strong>30 November 2024</strong><br>On this day, Israeli forces bombed an aid convoy in Gaza <a href="https://www.aljazeera.com/news/2024/11/30/israel-kills-world-central-kitchen-aid-workers-in-gaza">【source】</a>, killing three workers from the World Central Kitchen (WCK) and at least two others.</p><iframe height="250" src="https://www.youtube.com/embed/4gQcFBbfk_4" style="width:100%;"></iframe><p>This time, the victims were not Palestinians. They were foreign aid workers. And suddenly, the reactions changed. Western governments, who had stayed silent for months, started making loud statements:</p><ul><li><p><strong>United States</strong>: President Joe Biden said he was “outraged and heartbroken.” He admitted that Israel had not done enough to protect civilians and aid workers. He even warned that future U.S. support could depend on how Israel responds.</p></li><li><p><strong>United Kingdom, France, and Canada</strong>: These countries made their strongest criticism of Israel so far. They called the situation in Gaza “disproportionate” and “intolerable.” They demanded a ceasefire, the full return of humanitarian aid, and even mentioned possible sanctions or recognizing the State of Palestine if Israel didn’t change course.</p></li><li><p><strong>European Union</strong>: The EU, along with 24 other donor countries, demanded an immediate ceasefire and full humanitarian access. Some countries, like the Netherlands and Sweden, talked about reviewing trade deals and punishing Israeli leaders for war crimes.</p></li></ul><p>It sounded serious. It sounded like things were about to change.</p><p><strong>But just a few months later…</strong><br>Nothing changed. Israel kept killing, the destruction in Gaza kept growing, and the same governments went back to supporting Israel—giving it money, weapons, and political cover.</p><p><strong>21 May 2025</strong><br>Israeli soldiers fired at a group of international diplomats visiting the Jenin refugee camp in the West Bank [<a href="https://www.aljazeera.com/news/2025/5/21/eu-arab-diplomats-come-under-israeli-fire-in-occupied-west-banks-jenin">source</a>]. These were not Palestinian civilians—they were officials from the EU, Arab countries, Asia, and the West.</p><ul><li><strong>France</strong> called the shooting “unjustifiable and unacceptable.”</li><li><strong>Germany</strong> strongly condemned the attack and said the visit had been coordinated with Israeli forces.</li><li><strong>France, Italy, and Portugal</strong> summoned Israeli ambassadors for explanations.</li></ul><p>But let’s be honest.<br>Just like before, these words won’t last.<br>These governments will soon move on and go back to business as usual with Israel—just like they always do.</p><h3 id="The-Hypocrisy-is-Clear"><a href="#The-Hypocrisy-is-Clear" class="headerlink" title="The Hypocrisy is Clear"></a>The Hypocrisy is Clear</h3><p>When Palestinians are killed, Western governments largely remain silent. When others are harmed—especially foreigners—suddenly, there’s outrage, statements, and demands. This pattern is consistent. The response depends on who is affected, not what is happening. This shows a double standard in how governments apply international law and human rights. Despite repeated violations and ongoing violence, these same governments continue to support Israel politically and militarily. Public statements may shift temporarily when international personnel are harmed, but in practice, little changes.</p><p>At the same time, public opinion is shifting. People across many countries are paying closer attention, asking questions, and showing support for Palestinians. While governments may act based on politics and alliances, many individuals are pushing for a more consistent and fair approach. This growing awareness may not change government policies overnight—but it makes it harder for them to justify ongoing double standards.</p><p>So to the people—<strong>thank you!</strong> Your solidarity means everything.<br>And to the governments—<strong>shame on you!</strong> Your double standards are clear, and history will remember.</p>]]></content>
    
    
    <summary type="html">&lt;div&gt;&lt;/div&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>How to detect animals poses using mmpose</title>
    <link href="https://abozanona.me/detect-animals-poses-using-mmpose/"/>
    <id>https://abozanona.me/detect-animals-poses-using-mmpose/</id>
    <published>2025-04-04T21:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.729Z</updated>
    
    <content type="html"><![CDATA[<p>In this post I will demonstrate how to setup mmpose inside a docker container to detect animals poses using the pre-trained COCO-animal pose estimation model from mmpose.</p><span id="more"></span><p>MMPose is an open-source pose estimation toolbox built on PyTorch and part of the OpenMMLab ecosystem. It supports a wide range of pose estimation tasks, including 2D and 3D human and animal pose estimation.</p><p>Ps: You can refer to <a href="https://mmpose.readthedocs.io/en/latest/demos.html">https://mmpose.readthedocs.io/en/latest/demos.html</a> for more details on animals pose estimation using mmpose.</p><p>Here are the steps to do that:</p><h2 id="Setup-Docker-container-file"><a href="#Setup-Docker-container-file" class="headerlink" title="Setup Docker container file"></a>Setup Docker container file</h2><p>To get started create a file named <code>Dockerfile</code> with the following content</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.10</span>-slim</span><br><span class="line"></span><br><span class="line"><span class="comment"># Install system dependencies and build tools</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; apt-get install -y git build-essential gcc g++ libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev wget ffmpeg &amp;&amp; apt-get clean &amp;&amp; <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Install PyTorch for CPU</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Create a working directory</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Install MMPose dependencies</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir opencv-python</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir openmim</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir mmengine</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir -U mmcv==2.1.0</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir mmdet</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip3 install --no-cache-dir mmpose</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Make directories for input/output</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /app/input /app/output</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Download model checkpoint =&gt; This will save time and prevent python code from downloading them each time the code is executed.</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /root/.cache/torch/hub/checkpoints/ &amp;&amp; <span class="built_in">cd</span> /root/.cache/torch/hub/checkpoints/ &amp;&amp; wget -q https://download.openmmlab.com/mmpose/v1/projects/rtmposev1/rtmpose-m_simcc-ap10k_pt-aic-coco_210e-256x256-7a041aa1_20230206.pth</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /root/.cache/torch/hub/checkpoints/ &amp;&amp; <span class="built_in">cd</span> /root/.cache/torch/hub/checkpoints/ &amp;&amp; wget -q https://download.openmmlab.com/mmdetection/v3.0/rtmdet/rtmdet_m_8xb32-300e_coco/rtmdet_m_8xb32-300e_coco_20220719_112220-229f527c.pth</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Clone MMPose for configs and demo scripts. We will use it later in the code</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> git <span class="built_in">clone</span> -b main --depth 1 https://github.com/open-mmlab/mmpose.git /app/mmpose</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;/bin/bash&quot;</span>]</span></span><br></pre></td></tr></table></figure><h2 id="Add-code-to-detect-poses"><a href="#Add-code-to-detect-poses" class="headerlink" title="Add code to detect poses"></a>Add code to detect poses</h2><p>Next step is creating a file named <code>animal_pose.py</code> with the following content. This file holds the logic for pose estimation.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="keyword">import</span> mmcv</span><br><span class="line"><span class="keyword">from</span> mmpose.utils <span class="keyword">import</span> register_all_modules</span><br><span class="line"><span class="keyword">from</span> mmpose.apis <span class="keyword">import</span> MMPoseInferencer</span><br><span class="line"></span><br><span class="line">input_dir = <span class="string">&#x27;input&#x27;</span></span><br><span class="line">output_dir = <span class="string">&#x27;output&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    os.makedirs(output_dir, exist_ok=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    register_all_modules()</span><br><span class="line">    inferencer = MMPoseInferencer(<span class="string">&#x27;animal&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    input_files = [f <span class="keyword">for</span> f <span class="keyword">in</span> os.listdir(input_dir) </span><br><span class="line">                  <span class="keyword">if</span> f.lower().endswith((<span class="string">&#x27;.png&#x27;</span>, <span class="string">&#x27;.jpg&#x27;</span>, <span class="string">&#x27;.jpeg&#x27;</span>, <span class="string">&#x27;.mp4&#x27;</span>))]</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> input_files:</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> input_file <span class="keyword">in</span> input_files:</span><br><span class="line">        input_path = os.path.join(input_dir, input_file)</span><br><span class="line">        <span class="keyword">if</span> input_path.endswith(<span class="string">&quot;.mp4&quot;</span>):</span><br><span class="line">            process_video(input_path, inferencer)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            process_image(input_path, inferencer)</span><br><span class="line"></span><br><span class="line">    </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">process_video</span>(<span class="params">input_path, inferencer</span>):</span><br><span class="line">    video = mmcv.VideoReader(input_path)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> frame_idx, frame <span class="keyword">in</span> <span class="built_in">enumerate</span>(video):</span><br><span class="line">        result_generator = inferencer(frame, vis_out_dir=output_dir)</span><br><span class="line">        result = <span class="built_in">next</span>(result_generator)</span><br><span class="line"></span><br><span class="line">        predictions = result[<span class="string">&quot;predictions&quot;</span>][<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">        keypoints = np.array(predictions[<span class="string">&quot;keypoints&quot;</span>])</span><br><span class="line">        keypoint_scores = np.array(predictions[<span class="string">&quot;keypoint_scores&quot;</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">process_image</span>(<span class="params">input_path, inferencer</span>):</span><br><span class="line">    result_generator = inferencer(input_path, vis_out_dir=output_dir)</span><br><span class="line">    result = <span class="built_in">next</span>(result_generator)</span><br><span class="line">    </span><br><span class="line">    predictions = result[<span class="string">&quot;predictions&quot;</span>][<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">    keypoints = np.array(predictions[<span class="string">&quot;keypoints&quot;</span>])</span><br><span class="line">    keypoint_scores = np.array(predictions[<span class="string">&quot;keypoint_scores&quot;</span>])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><p>This code will detect poses for both videos and images.</p><h2 id="Link-everything-together"><a href="#Link-everything-together" class="headerlink" title="Link everything together"></a>Link everything together</h2><p>And finally create a file named <code>run.sh</code> with the following content. This bash script build the docker image, moves files to it, and executes the python script. The output result for input images can be found in out directory.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># Check if file provided</span></span><br><span class="line"><span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$1</span>&quot;</span> == <span class="string">&quot;&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Usage: ./direct_run.sh /path/to/animal_image.jpg&quot;</span></span><br><span class="line">    <span class="built_in">exit</span> 1</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Create directories</span></span><br><span class="line"><span class="built_in">mkdir</span> -p input output</span><br><span class="line"></span><br><span class="line"><span class="comment"># Copy image to input</span></span><br><span class="line"><span class="built_in">cp</span> <span class="string">&quot;<span class="variable">$1</span>&quot;</span> input/</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Copied <span class="variable">$1</span> to input directory&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Build the Docker image</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Building Docker image...&quot;</span></span><br><span class="line">docker build --platform linux/arm64 -t mmpose-animal-m2 .</span><br><span class="line"></span><br><span class="line"><span class="comment"># Run inference directly (non-interactive mode)</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Running animal pose detection...&quot;</span></span><br><span class="line">docker run --<span class="built_in">rm</span> --platform linux/arm64 \</span><br><span class="line">    -v <span class="string">&quot;<span class="subst">$(pwd)</span>/input:/app/input&quot;</span> \</span><br><span class="line">    -v <span class="string">&quot;<span class="subst">$(pwd)</span>/output:/app/output&quot;</span> \</span><br><span class="line">    -v <span class="string">&quot;<span class="subst">$(pwd)</span>/animal_pose.py:/app/animal_pose.py&quot;</span> \</span><br><span class="line">    mmpose-animal-m2 python3 /app/animal_pose.py</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Processing complete! Check the &#x27;output&#x27; directory for results.&quot;</span></span><br></pre></td></tr></table></figure><p>If you want to get the poses coordinates, you can print <code>keypoints</code> &amp; <code>keypoint_scores</code> and use them.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;In this post I will demonstrate how to setup mmpose inside a docker container to detect animals poses using the pre-trained COCO-animal pose estimation model from mmpose.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>Executing JavaScript in Shared Hosting (cPanel) Using PHP</title>
    <link href="https://abozanona.me/how-to-evaluate-js-from-php-in-cpanel/"/>
    <id>https://abozanona.me/how-to-evaluate-js-from-php-in-cpanel/</id>
    <published>2025-03-31T21:23:23.000Z</published>
    <updated>2025-12-01T01:02:39.238Z</updated>
    
    <content type="html"><![CDATA[<p>In some cPanel environments where your website is hosted on shared hosting, it can be <strong>very difficult</strong> to find a way to evaluate JavaScript. In this post, I’ll share a method I discovered that allows you to execute JS on a shared host directly from PHP code.</p><span id="more"></span><p>The command you are looking for is <code>js</code>.</p><p>If you run <code>js -v</code> in the terminal from cpanel dashboard, you’ll see this output:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ js -v</span><br><span class="line">JavaScript-C 1.8.5 2011-03-31</span><br><span class="line">usage: js [options] [scriptfile] [scriptarg...]</span><br><span class="line">Options:</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>Even if the cPanel admin doesn’t allow you to install Node.js or another JS engine, the <code>js</code> command is usually available.</p><p>The command is <strong>very limited</strong>—it doesn’t support modern JavaScript syntax. It appears to be based on an older version of JavaScript (likely ES5 or earlier). For example:</p><ul><li>Missing <code>string.replaceAll()</code></li><li>No <code>console.log()</code></li><li>No support for <code>async</code> functions</li></ul><p>To work around this, you’ll need to:</p><ol><li>Manually implement missing features.</li><li>Rewrite parts of your code to be compatible.</li></ol><p>The biggest challenge is retrieving the output of your JS code. Since <code>console.log()</code> doesn’t work, I found an unconventional solution: <strong>throwing an error</strong> with the result.</p><p>When you throw an error, it prints to the console, and you can capture it from PHP.</p><p>Here’s a working example:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">executeJs</span>(<span class="params"><span class="variable">$jsCode</span></span>) </span>&#123;</span><br><span class="line">    <span class="comment">// Add missing functionality</span></span><br><span class="line">    <span class="variable">$replaceAllString</span> = <span class="string">&quot;</span></span><br><span class="line"><span class="string">        String.prototype.replaceAll = function(match, replace) &#123;</span></span><br><span class="line"><span class="string">            return this.replace(new RegExp(match, &#x27;g&#x27;), function() &#123; return replace &#125;);</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">    &quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Combine with user&#x27;s JS code</span></span><br><span class="line">    <span class="variable">$jsCode</span> = <span class="variable">$replaceAllString</span> . <span class="variable">$jsCode</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Create a temporary file</span></span><br><span class="line">    <span class="variable">$tempFile</span> = <span class="title function_ invoke__">tmpfile</span>();</span><br><span class="line">    <span class="variable">$metaData</span> = <span class="title function_ invoke__">stream_get_meta_data</span>(<span class="variable">$tempFile</span>);</span><br><span class="line">    <span class="variable">$tmpFilename</span> = <span class="variable">$metaData</span>[<span class="string">&#x27;uri&#x27;</span>];</span><br><span class="line">    <span class="title function_ invoke__">fwrite</span>(<span class="variable">$tempFile</span>, <span class="variable">$jsCode</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Execute JS and capture output</span></span><br><span class="line">    <span class="variable">$cmd</span> = <span class="string">&quot;/bin/js -f &quot;</span> . <span class="title function_ invoke__">escapeshellarg</span>(<span class="variable">$tmpFilename</span>) . <span class="string">&quot; 2&gt;&amp;1&quot;</span>;</span><br><span class="line">    <span class="variable">$result</span> = <span class="title function_ invoke__">shell_exec</span>(<span class="variable">$cmd</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Clean up the output</span></span><br><span class="line">    <span class="variable">$result</span> = <span class="title function_ invoke__">str_replace</span>(<span class="string">&quot;uncaught exception: &quot;</span>, <span class="string">&quot;&quot;</span>, <span class="variable">$result</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_ invoke__">trim</span>(<span class="variable">$result</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This method isn’t perfect, but it’s a useful workaround when you’re restricted to shared hosting.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;In some cPanel environments where your website is hosted on shared hosting, it can be &lt;strong&gt;very difficult&lt;/strong&gt; to find a way to evaluate JavaScript. In this post, I’ll share a method I discovered that allows you to execute JS on a shared host directly from PHP code.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>How to Add Integration Tests to a Golang Gin Project</title>
    <link href="https://abozanona.me/how-to-add-integration-tests-to-golang-gin/"/>
    <id>https://abozanona.me/how-to-add-integration-tests-to-golang-gin/</id>
    <published>2025-03-04T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.730Z</updated>
    
    <content type="html"><![CDATA[<p>Integration testing helps ensure that different parts of your application work correctly together. This guide will show you how to add integration tests to a Golang project using the Gin framework.</p><span id="more"></span><h2 id="Required-Packages"><a href="#Required-Packages" class="headerlink" title="Required Packages"></a>Required Packages</h2><p>Before starting, install the necessary packages:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ go get github.com/stretchr/testify/suite</span><br><span class="line">$ go get github.com/testcontainers/testcontainers-go</span><br></pre></td></tr></table></figure><h2 id="Setting-Up-the-Server"><a href="#Setting-Up-the-Server" class="headerlink" title="Setting Up the Server"></a>Setting Up the Server</h2><p>Create a function to return the Gin server:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetServer</span><span class="params">()</span></span> *http.Server &#123;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">routersInit := routers.InitRouter()</span><br><span class="line">server := &amp;http.Server&#123;</span><br><span class="line">Addr:           endPoint,</span><br><span class="line">Handler:        routersInit,</span><br><span class="line">ReadTimeout:    readTimeout,</span><br><span class="line">WriteTimeout:   writeTimeout,</span><br><span class="line">MaxHeaderBytes: maxHeaderBytes,</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> server</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Start the server normally in your main file:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">server := GetServer()</span><br><span class="line">err := server.ListenAndServe()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;Server err: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Creating-a-Test-Database-with-Testcontainers"><a href="#Creating-a-Test-Database-with-Testcontainers" class="headerlink" title="Creating a Test Database with Testcontainers"></a>Creating a Test Database with Testcontainers</h2><p>To run integration tests, use Testcontainers to set up a PostgreSQL database. The connection string retrieved from the test container should be used later when connecting to the database, depending on whether an integration test is running.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> testintegration</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;log&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/testcontainers/testcontainers-go&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/testcontainers/testcontainers-go/modules/postgres&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/testcontainers/testcontainers-go/wait&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> PostgresContainer <span class="keyword">struct</span> &#123;</span><br><span class="line">*postgres.PostgresContainer</span><br><span class="line">ConnectionString <span class="type">string</span></span><br><span class="line">Server           *http.Server</span><br><span class="line">ServerCancel     context.CancelFunc</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">CreatePostgresContainer</span><span class="params">(ctx context.Context)</span></span> (*PostgresContainer, <span class="type">error</span>) &#123;</span><br><span class="line">pgContainer, err := postgres.RunContainer(ctx,</span><br><span class="line">testcontainers.WithImage(<span class="string">&quot;postgres:15.3-alpine&quot;</span>),</span><br><span class="line">postgres.WithInitScripts(filepath.Join(<span class="string">&quot;..&quot;</span>, <span class="string">&quot;migrations&quot;</span>, <span class="string">&quot;1728047791947_initial.sql&quot;</span>)),</span><br><span class="line">testcontainers.WithWaitStrategy(</span><br><span class="line">wait.ForLog(<span class="string">&quot;database system is ready to accept connections&quot;</span>).WithOccurrence(<span class="number">2</span>).WithStartupTimeout(<span class="number">5</span>*time.Second)),</span><br><span class="line">)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">connStr, err := pgContainer.ConnectionString(ctx, <span class="string">&quot;sslmode=disable&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ctx, cancel := context.WithCancel(context.Background())</span><br><span class="line">server := GetServer()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">&lt;-ctx.Done()</span><br><span class="line"><span class="keyword">if</span> err := server.Shutdown(context.Background()); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;Server shutdown error: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> err := server.ListenAndServe(); err != <span class="literal">nil</span> &amp;&amp; err != http.ErrServerClosed &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;Server error: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> &amp;PostgresContainer&#123;</span><br><span class="line">PostgresContainer: pgContainer,</span><br><span class="line">ConnectionString:  connStr,</span><br><span class="line">Server:            server,</span><br><span class="line">ServerCancel:      cancel,</span><br><span class="line">&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Writing-an-Integration-Test-Suite"><a href="#Writing-an-Integration-Test-Suite" class="headerlink" title="Writing an Integration Test Suite"></a>Writing an Integration Test Suite</h2><p>Use <code>testify/suite</code> to manage test setup and teardown.</p><ul><li><strong>Setup</strong>: We create a test database using Testcontainers and start the test server.</li><li><strong>Teardown</strong>: We terminate the test database and shut down the test server.</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> testintegration</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;encoding/json&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;io/ioutil&quot;</span></span><br><span class="line"><span class="string">&quot;log&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/stretchr/testify/suite&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> ProjectTestSuite <span class="keyword">struct</span> &#123;</span><br><span class="line">suite.Suite</span><br><span class="line">pgContainer *PostgresContainer</span><br><span class="line">ctx         context.Context</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(suite *ProjectTestSuite)</span></span> SetupSuite() &#123;</span><br><span class="line">suite.ctx = context.Background()</span><br><span class="line">pgContainer, err := CreatePostgresContainer(suite.ctx)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line">suite.pgContainer = pgContainer</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(suite *ProjectTestSuite)</span></span> TearDownSuite() &#123;</span><br><span class="line"><span class="keyword">if</span> err := suite.pgContainer.Terminate(suite.ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatalf(<span class="string">&quot;error terminating postgres container: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">suite.pgContainer.ServerCancel()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *ProjectTestSuite)</span></span> TestGetEndpointResponse(endpoint <span class="type">string</span>, expectedStruct <span class="keyword">interface</span>&#123;&#125;) &#123;</span><br><span class="line">url := fmt.Sprintf(<span class="string">&quot;%s%s&quot;</span>, <span class="string">&quot;http://localhost:8000&quot;</span>, endpoint)</span><br><span class="line"></span><br><span class="line">resp, err := http.Get(url)</span><br><span class="line">s.Require().NoError(err, <span class="string">&quot;Failed to send GET request&quot;</span>)</span><br><span class="line"><span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line"></span><br><span class="line">s.Equal(http.StatusOK, resp.StatusCode, <span class="string">&quot;Unexpected status code&quot;</span>)</span><br><span class="line"></span><br><span class="line">body, err := ioutil.ReadAll(resp.Body)</span><br><span class="line">s.Require().NoError(err, <span class="string">&quot;Failed to read response body&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> actualResponse <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">err = json.Unmarshal(body, &amp;actualResponse)</span><br><span class="line">s.Require().NoError(err, <span class="string">&quot;Failed to unmarshal response body&quot;</span>)</span><br><span class="line"></span><br><span class="line">expectedJSON, err := json.Marshal(expectedStruct)</span><br><span class="line">s.Require().NoError(err, <span class="string">&quot;Failed to marshal expected struct&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> expectedResponse <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">err = json.Unmarshal(expectedJSON, &amp;expectedResponse)</span><br><span class="line">s.Require().NoError(err, <span class="string">&quot;Failed to unmarshal expected JSON&quot;</span>)</span><br><span class="line"></span><br><span class="line">s.Equal(expectedResponse, actualResponse, <span class="string">&quot;Response does not match expected struct&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestProjectTestSuite</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">suite.Run(t, <span class="built_in">new</span>(ProjectTestSuite))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Example-Test-Case"><a href="#Example-Test-Case" class="headerlink" title="Example Test Case"></a>Example Test Case</h3><p>To test an endpoint, call it with the expected response. The function will send a request to the given endpoint, retrieve the response, and compare it to the expected output.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> testintegration</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(suite *ProjectTestSuite)</span></span> TestGetEndpoint() &#123;</span><br><span class="line">suite.TestGetEndpointResponse(<span class="string">&quot;/api/web/v1/videos&quot;</span>, <span class="string">&quot;expected_response&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Running-the-Tests"><a href="#Running-the-Tests" class="headerlink" title="Running the Tests"></a>Running the Tests</h2><p>To run the integration tests, use:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ go <span class="built_in">test</span> -v ./...</span><br></pre></td></tr></table></figure><p>This will execute all tests in your project.</p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>By setting up integration tests with Testcontainers and <code>testify/suite</code>, you can ensure that your Golang Gin project works correctly with a real database. This approach makes it easier to catch issues early and maintain the quality of your application.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Integration testing helps ensure that different parts of your application work correctly together. This guide will show you how to add integration tests to a Golang project using the Gin framework.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>How to collect Users&#39; addresses in Stripe</title>
    <link href="https://abozanona.me/how-to-collect-zip-code-from-stripe/"/>
    <id>https://abozanona.me/how-to-collect-zip-code-from-stripe/</id>
    <published>2025-02-19T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.730Z</updated>
    
    <content type="html"><![CDATA[<p>If you’re working with Stripe and realize that the account didn’t collect old users’ billing addresses, you might need a way to retrieve them. This can be important for legal or compliance reasons.</p><span id="more"></span><p>Luckily, Stripe does collect some payment-related information, including the IP address from where a payment was made. While Stripe doesn’t show this IP address directly, there is a way to retrieve it.</p><h2 id="Getting-the-User’s-IP-Address-from-Stripe"><a href="#Getting-the-User’s-IP-Address-from-Stripe" class="headerlink" title="Getting the User’s IP Address from Stripe"></a>Getting the User’s IP Address from Stripe</h2><ol><li><p><strong>Subscribe to Stripe’s Fraud Teams</strong> – You can sign up for a one-month free trial of <a href="https://stripe.com/en-de/radar/fraud-teams">Stripe Radar for Fraud Teams</a>. This will give you access to additional fraud detection details, including user IP addresses.</p></li><li><p><strong>Find the IP Address</strong> – After subscribing:</p><ul><li>Go to the <a href="https://dashboard.stripe.com/payments">Transactions</a> page.</li><li>Filter records by the user’s email address.</li><li>Click on any payment for that user.</li><li>Click <strong>Show all insights</strong> to open a popup.</li><li>In the <strong>Checkout details</strong> section, you’ll find the user’s IP address.</li></ul></li></ol><h2 id="Automating-the-Process-with-Stripe-API"><a href="#Automating-the-Process-with-Stripe-API" class="headerlink" title="Automating the Process with Stripe API"></a>Automating the Process with Stripe API</h2><p>Manually collecting users’ zip codes one by one would take forever. Fortunately, we can automate this using Stripe’s API.</p><p><strong>Step 1: Get Subscription Users</strong><br>Use the <code>/v1/subscriptions</code> endpoint to fetch all subscribers:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET /v1/subscriptions</span><br></pre></td></tr></table></figure><p><strong>Step 2: Get Payment Intent IDs</strong><br>For each user, retrieve their payment intent IDs using:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET /v1/payment_intents</span><br></pre></td></tr></table></figure><p><strong>Step 3: Fetch IP Address from Payment Intent</strong><br>Once you have the <code>paymentIntentID</code>, call this API endpoint to get IP address details:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET /v1/payment_intents/<span class="variable">$&#123;paymentIntentID&#125;</span>?<span class="built_in">expand</span>%5B%5D=<span class="built_in">source</span>&amp;<span class="built_in">expand</span>%5B%5D=customer&amp;<span class="built_in">expand</span>%5B%5D=payment_method&amp;include_only%5B%5D=payment_method.billing_details.address%2Cpayment_method.card.country</span><br></pre></td></tr></table></figure><p>This will return:</p><ul><li>The user’s <strong>IP address</strong></li><li>The <strong>country code</strong></li></ul><h2 id="Using-IP-Geolocation-to-Get-Address-Details"><a href="#Using-IP-Geolocation-to-Get-Address-Details" class="headerlink" title="Using IP Geolocation to Get Address Details"></a>Using IP Geolocation to Get Address Details</h2><p>With the IP address, we can determine the user’s approximate zip code and country.</p><p>One option is to use <a href="https://ip-api.com/">ip-api.com</a>, which provides geolocation data based on an IP address. Simply send a request like this:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET http://ip-api.com/json/&#123;IP_ADDRESS&#125;</span><br></pre></td></tr></table></figure><p>The response will include details like:</p><ul><li>Country</li><li>Region</li><li>City</li><li>Zip code</li></ul><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>If your Stripe account isn’t collecting users’ billing addresses and you want to collect them, this method allows you to retrieve them using payment IPs and geolocation services. While it requires subscribing to Fraud Teams for a short period, it provides a useful workaround to collect addresses for users.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;If you’re working with Stripe and realize that the account didn’t collect old users’ billing addresses, you might need a way to retrieve them. This can be important for legal or compliance reasons.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>My workout playlist</title>
    <link href="https://abozanona.me/my-workout-playlist-2024/"/>
    <id>https://abozanona.me/my-workout-playlist-2024/</id>
    <published>2025-02-15T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.732Z</updated>
    
    <content type="html"><![CDATA[<p>Maintaining my physical and mental health is a priority, and running and working out have become key components of that. I try to make time for exercise after work or on weekends when the weather is good and when I have time. It’s a great way to de-stress and stay in shape after long hours at a desk.</p><span id="more"></span><p>I’m sharing a part of my current workout playlist, based on recommendations from my music app. Hopefully, you’ll find some inspiration for your own workouts.</p><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"><style>    .player-container {        width: 640px;        max-width: 100%;        margin: 0 auto;        background-color: #fff;        border-radius: 10px;        box-shadow: 0 0 10px rgba(0,0,0,0.1);        padding: 20px;    }    #player {        display: none;    }    .video-list {        margin-bottom: 20px;    }    .video-item {        display: flex;        align-items: center;        margin-bottom: 10px;        padding: 10px;        border-radius: 5px;        transition: background-color 0.3s;        justify-content: space-between; /* Add this */    }    .video-item:hover {        background-color: #f0f0f0;    }    .video-item.active {        background-color: #e0e0e0;    }    .video-item-info {        display: flex;        align-items: center;        cursor: pointer; /* Make the thumbnail and title clickable */    }    .video-item img {        width: 120px;        height: 90px;        margin-right: 10px;        border-radius: 5px;    }    .controls {        display: flex;        align-items: center;        justify-content: space-between;        margin-bottom: 20px;    }    #progress-bar {        flex-grow: 1;        margin: 0 10px;        -webkit-appearance: none;        height: 5px;        border-radius: 5px;        background: #d3d3d3;        outline: none;    }    #progress-bar::-webkit-slider-thumb {        -webkit-appearance: none;        appearance: none;        width: 15px;        height: 15px;        border-radius: 50%;        background: #97dffd;        cursor: pointer;    }    button {        background-color: transparent;        border: none;        cursor: pointer;        font-size: 24px;        color: #97dffd;    }    .time-display {        font-size: 14px;        color: #666;    }    .youtube-link-button {        background-color: #e74c3c; /* Red */        color: white;        border: none;        padding: 8px 12px;        border-radius: 5px;        cursor: pointer;        font-size: 14px;        transition: background-color 0.3s;    }    .youtube-link-button:hover {        background-color: #c0392b; /* Darker red */    }    .fluid-vids {        padding: 0 !important;        padding-top: 0px !important;    }</style><div class="player-container">        <div class="controls">        <button id="prev"><i class="fas fa-step-backward"></i></button>        <button id="play-pause"><i class="fas fa-play"></i></button>        <button id="next"><i class="fas fa-step-forward"></i></button>        <input type="range" id="progress-bar" min="0" max="100" value="0">        <span class="time-display">            <span id="current-time">0:00</span> / <span id="duration">0:00</span>        </span>    </div>    <div id="player"></div>    <div class="video-list" id="video-list"></div></div><script src="https://www.youtube.com/iframe_api"></script><script>    let player;    let progressBar;    let playPauseButton;    let currentTimeSpan;    let durationSpan;    let playlist = ['ApXoWvfEYVU', 'vjFoj6nSPTM', '3UbjgjfYC-I', 'E9T78bT26sk', '4CSokyrdVgE', 'tLsJQ5srVQA', 'aSZUJJWXzAU', 'Zasx9hjo4WY', 't_N88_sndVs', 'ug0L5S2Qzwg', 'SJKeDG8lw04', 'x6JJTVol-iI', 'qAgPH1CWiAw', 'pgcZJqfCL80', 'HYsz1hP0BFo', 'RIDiNoRD69k', '_gPDULwKOkY'];    let currentVideoIndex = 0;    let isPlayerReady = false;    let videoListCreated = false;    function onYouTubeIframeAPIReady() {        player = new YT.Player('player', {            height: '360',            width: '640',            videoId: '',            events: {                'onReady': onPlayerReady,                'onStateChange': onPlayerStateChange            }        });    }    function onPlayerReady(event) {        isPlayerReady = true;        progressBar = document.getElementById('progress-bar');        playPauseButton = document.getElementById('play-pause');        currentTimeSpan = document.getElementById('current-time');        durationSpan = document.getElementById('duration');        playPauseButton.addEventListener('click', togglePlayPause);        progressBar.addEventListener('input', seekTo);        document.getElementById('prev').addEventListener('click', playPreviousVideo);        document.getElementById('next').addEventListener('click', playNextVideo);        setInterval(updateProgressBar, 1000);        createVideoList().then(() => {            videoListCreated = true;        });    }    function onPlayerStateChange(event) {        if (event.data == YT.PlayerState.ENDED) {            playNextVideo();        }    }    function togglePlayPause() {        if (!videoListCreated) return;        if (player.getPlayerState() == YT.PlayerState.PLAYING) {            player.pauseVideo();            playPauseButton.innerHTML = '<i class="fas fa-play"></i>';        } else {            // Start playing from the beginning if no video is loaded, or resume            if (player.getPlayerState() == YT.PlayerState.UNSTARTED || player.getVideoData().video_id === undefined) {                loadVideo(playlist[currentVideoIndex]);            } else {                player.playVideo();            }            playPauseButton.innerHTML = '<i class="fas fa-pause"></i>';        }    }    function seekTo() {        if (!videoListCreated) return;        const seekToTime = player.getDuration() * (progressBar.value / 100);        player.seekTo(seekToTime, true);    }    function updateProgressBar() {        if (!videoListCreated) return;        if (player && player.getCurrentTime && player.getDuration) {            const currentTime = player.getCurrentTime();            const duration = player.getDuration();            const percentage = (currentTime / duration) * 100;            progressBar.value = percentage;            currentTimeSpan.textContent = formatTime(currentTime);            durationSpan.textContent = formatTime(duration);        }    }    function formatTime(time) {        const minutes = Math.floor(time / 60);        const seconds = Math.floor(time % 60);        return `${minutes}:${seconds.toString().padStart(2, '0')}`;    }    function loadVideo(videoId) {        if (isPlayerReady && player && player.loadVideoById) {            player.loadVideoById(videoId);            updateActiveVideo();            playPauseButton.innerHTML = '<i class="fas fa-pause"></i>';        } else {            console.error('Player not ready');        }    }    function playPreviousVideo() {        if (!videoListCreated) return;        currentVideoIndex = (currentVideoIndex - 1 + playlist.length) % playlist.length;        loadVideo(playlist[currentVideoIndex]);    }    function playNextVideo() {        if (!videoListCreated) return;        currentVideoIndex = (currentVideoIndex + 1) % playlist.length;        loadVideo(playlist[currentVideoIndex]);    }    async function createVideoList() {        const videoList = document.getElementById('video-list');        videoList.innerHTML = ''; // Clear existing items        for (let index = 0; index < playlist.length; index++) {            const videoId = playlist[index];            try {                const response = await fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${videoId}`);                const data = await response.json();                const videoItem = document.createElement('div');                videoItem.className = 'video-item';                // Make the thumbnail and title clickable                const videoItemInfo = document.createElement('div');                videoItemInfo.className = 'video-item-info';                videoItemInfo.innerHTML = `                    <img src="${data.thumbnail_url}" alt="${data.title}">                    <span>${data.title}</span>                `;                videoItemInfo.addEventListener('click', () => {                    currentVideoIndex = index;                    loadVideo(videoId);                });                videoItem.appendChild(videoItemInfo);                // YouTube link button                const youtubeLinkButton = document.createElement('button');                youtubeLinkButton.className = 'youtube-link-button';                youtubeLinkButton.innerHTML = '<i class="fab fa-youtube"></i>';                youtubeLinkButton.addEventListener('click', (event) => {                    event.stopPropagation(); // Prevent triggering video selection                    window.open(`https://www.youtube.com/watch?v=${videoId}`, '_blank');                });                videoItem.appendChild(youtubeLinkButton);                videoList.appendChild(videoItem);            } catch (error) {                console.error('Error fetching video data:', error);            }        }    }    function updateActiveVideo() {        if (!videoListCreated) return;        const videoItems = document.querySelectorAll('.video-item');        videoItems.forEach((item, index) => {            if (index === currentVideoIndex) {                item.classList.add('active');            } else {                item.classList.remove('active');            }        });    }</script>]]></content>
    
    
    <summary type="html">&lt;p&gt;Maintaining my physical and mental health is a priority, and running and working out have become key components of that. I try to make time for exercise after work or on weekends when the weather is good and when I have time. It’s a great way to de-stress and stay in shape after long hours at a desk.&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>Integrate Pac-Man Contribution Graph into Your GitHub Profile</title>
    <link href="https://abozanona.me/integrate-pacman-contribution-graph-into-github/"/>
    <id>https://abozanona.me/integrate-pacman-contribution-graph-into-github/</id>
    <published>2025-02-08T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.731Z</updated>
    
    <content type="html"><![CDATA[<p>Want to add a fun and interactive touch to your GitHub profile? You can integrate a <strong>Pac-Man game</strong> into your contribution graph using GitHub Actions! This method automatically updates your profile every 24 hours with a Pac-Man animation that plays across your contributions. Here’s how to set it up:</p><span id="more"></span><img loading="lazy" src="https://raw.githubusercontent.com/abozanona/abozanona/output/pacman-contribution-graph-dark.svg"/><h2 id="Step-1-Create-a-New-Repository"><a href="#Step-1-Create-a-New-Repository" class="headerlink" title="Step 1: Create a New Repository"></a><strong>Step 1: Create a New Repository</strong></h2><p>Start by creating a new repository with the <strong>exact same name</strong> as your GitHub username. This special repository is what powers your GitHub profile page.</p><hr><h2 id="Step-2-Set-Up-GitHub-Actions"><a href="#Step-2-Set-Up-GitHub-Actions" class="headerlink" title="Step 2: Set Up GitHub Actions"></a><strong>Step 2: Set Up GitHub Actions</strong></h2><p>Inside your repository, create a <strong>workflow file</strong> to generate and update the Pac-Man game automatically.</p><ul><li>Navigate to your repository</li><li>Create a new directory: <code>.github/workflows/</code></li><li>Inside that folder, create a file called <code>main.yml</code></li><li>Add the following content:</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">generate</span> <span class="string">pacman</span> <span class="string">game</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">schedule:</span> <span class="comment"># Run automatically every 24 hours</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&quot;0 */24 * * *&quot;</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span> <span class="comment"># Allows manual triggering</span></span><br><span class="line">  <span class="attr">push:</span> <span class="comment"># Runs on every push to the main branch</span></span><br><span class="line">    <span class="attr">branches:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">generate:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">5</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">generate</span> <span class="string">pacman-contribution-graph.svg</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">abozanona/pacman-contribution-graph@main</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">github_user_name:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository_owner</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># Push the generated SVG to the output branch</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">push</span> <span class="string">pacman-contribution-graph.svg</span> <span class="string">to</span> <span class="string">the</span> <span class="string">output</span> <span class="string">branch</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">crazy-max/ghaction-github-pages@v3.1.0</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">target_branch:</span> <span class="string">output</span></span><br><span class="line">          <span class="attr">build_dir:</span> <span class="string">dist</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">GITHUB_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>This workflow will generate a <strong>Pac-Man contribution graph</strong> and update it automatically every day!</p><hr><h2 id="Step-3-Add-the-Pac-Man-Graph-to-Your-Profile-README"><a href="#Step-3-Add-the-Pac-Man-Graph-to-Your-Profile-README" class="headerlink" title="Step 3: Add the Pac-Man Graph to Your Profile README"></a><strong>Step 3: Add the Pac-Man Graph to Your Profile README</strong></h2><p>Now, let’s embed the generated graph into your profile.</p><ul><li>In your repository, create a <code>README.md</code> file (or edit an existing one).</li><li>Add the following content:</li></ul><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">picture</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">source</span> <span class="attr">media</span>=<span class="string">&quot;(prefers-color-scheme: dark)&quot;</span> <span class="attr">srcset</span>=<span class="string">&quot;https://raw.githubusercontent.com/[USERNAME]/[USERNAME]/output/pacman-contribution-graph-dark.svg&quot;</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">source</span> <span class="attr">media</span>=<span class="string">&quot;(prefers-color-scheme: light)&quot;</span> <span class="attr">srcset</span>=<span class="string">&quot;https://raw.githubusercontent.com/[USERNAME]/[USERNAME]/output/pacman-contribution-graph.svg&quot;</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">alt</span>=<span class="string">&quot;pacman contribution graph&quot;</span> <span class="attr">src</span>=<span class="string">&quot;https://raw.githubusercontent.com/[USERNAME]/[USERNAME]/output/pacman-contribution-graph.svg&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">picture</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="emphasis">_generated with [<span class="string">abozanona/pacman-contribution-graph</span>](<span class="link">https://abozanona.github.io/pacman-contribution-graph/</span>)_</span></span><br></pre></td></tr></table></figure><p>📌 <strong>Don’t forget to replace <code>[USERNAME]</code> with your actual GitHub username!</strong></p><hr><h2 id="Step-4-Push-Your-Changes-and-Enjoy"><a href="#Step-4-Push-Your-Changes-and-Enjoy" class="headerlink" title="Step 4: Push Your Changes and Enjoy!"></a><strong>Step 4: Push Your Changes and Enjoy!</strong></h2><p>Commit and push your changes to GitHub. Within a few minutes, your profile will display a <strong>Pac-Man game playing across your contribution graph!</strong> 🎮</p><hr><p>For an example in action, check out <a href="https://github.com/abozanona">abozanona’s GitHub repository</a>. Also have a look at the <a href="https://abozanona.github.io/pacman-contribution-graph/">project’s home page</a> to check other ways of integrating the game.</p><p>By following 👻these 👻steps,🟩you 👻 can make your 🟡GitHub 🟩profile more 👻fun and 🟩unique.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Want to add a fun and interactive touch to your GitHub profile? You can integrate a &lt;strong&gt;Pac-Man game&lt;/strong&gt; into your contribution graph using GitHub Actions! This method automatically updates your profile every 24 hours with a Pac-Man animation that plays across your contributions. Here’s how to set it up:&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>My voice from Germany for Palestine</title>
    <link href="https://abozanona.me/my-voice-from-germany-for-palestine/"/>
    <id>https://abozanona.me/my-voice-from-germany-for-palestine/</id>
    <published>2025-02-06T22:23:23.000Z</published>
    <updated>2025-11-23T22:15:07.574Z</updated>
    
    <content type="html"><![CDATA[<p>A long time ago, I was having a conversation with my mother about my country Palestine. I told her that the situation would get worse. She said, “This is our life. It is our destiny to live this struggle and fight it. For that, we were born in this land. This is our test.”</p><span id="more"></span><p>Her words came back to my mind many times this past year, especially during the demonstrations I participated in here in Hamburg, the war on Gaza, the destruction and the killing of innocent people. The Prophet Muhammad (peace be upon him) said, “How wonderful is the affair of the believer! Verily, all of his affairs are good, and this is not for anyone except the believer. If something good happens to him, he is grateful and that is good for him. If something bad happens to him, he is patient and that is good for him”. This hadith reflects the resilience of the people of Gaza. No matter how much they suffer, they continue to stand firm, resist, and hold onto hope. Their strength in the face of such devastation is a lesson for the world.</p><p>Since the start of the war on Gaza, I have participated in many protests in Hamburg. People from different backgrounds, religions, and nationalities came together to stand for Palestine. We marched through the streets, holding Palestinian flags, chanting for justice, and demanding an end to the genocide in Gaza. I have recorded many moments from the protests I attended in Hamburg. Here is a playlist of those videos:</p><a href="https://www.youtube.com/watch?v=tHCLuACxZqg&list=PLb9WCziei3SRXp6x4kGS06AQC6MjwwVBD&index=1" target="__blank"><div class="image-container"><img class="no-popup-img" loading="lazy" src="https://i.ytimg.com/vi/tHCLuACxZqg/hqdefault.jpg"/></div></a><style>.image-container {  position: relative;}.image-container::before {  content: "";  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);  width: 0;  height: 0;  border-style: solid;  border-width: 20px 0 20px 40px;  border-color: transparent transparent transparent #000000;}</style><p>Despite the massive support for Palestine worldwide, German goverment, like many Western goverments, continues its blind support for Israel. The government justifies this with what they call “guilt complex&#x2F;historical responsibility” Because of their history, they feel responsible for supporting Israel no matter what. But it makes no sense to support a genocide out of guilt. First, because today’s Germans did not commit the crimes of the past. And second, because supporting a new injustice does not correct an old one.</p><p>I have met many German friends who disagree with their government’s stance. They told me how this “historical responsibility” is taught in schools and from a young age. But things are changing. More and more people are questioning this narrative and standing up against all forms of injustice, including the genocide in Gaza.</p><iframe height="300" src="https://www.youtube.com/embed/dGj2DsvmDf8" style="width:100%;"></iframe><p>Also watch:</p><ul><li><a href="https://www.youtube.com/shorts/Ohm73zbhLuw">Daniel Gerlach: Gaza doesn’t exist anymore</a></li><li><a href="https://www.youtube.com/shorts/fnAr9w8M_cU">Daniel Gerlach: Palestinian journalists killed in Gaza</a></li><li><a href="https://www.youtube.com/watch?v=ANd2i-APX8M">Annalena Baerbock denies that Nasrallah had agreed to a ceasefire with Israel</a></li><li><a href="https://www.youtube.com/watch?v=UglhL70tmIo">German government’s human rights commissioner cannot name war crime committed by Israel in Gaza</a></li></ul><p>I am not afraid of being killed by an Israeli soldier. I know that if I die, I will go to a better place. What bothers me the most is that after I die, they will reduce me to a number. A single number added to the total death toll. But I’m not just a number, and each one of us is more than a number. We are lives, dreams, hopes, families, stories, and memories.</p><p>What gives me strength is the solidarity from people all over the world. Seeing people in different countries protesting, raising awareness, and demanding justice gives me hope that Palestine will one day be free.</p><p>One moment that touched me deeply was a video I saw of Italian tourists singing “Bella Ciao” on Shuhada Street in Hebron. This street is the one I used to take to high school. It runs through the heart of Hebron, where Israeli settlers have occupied the city center. It is now closed to Palestinians except for those who live there, and even they must show identification to pass.</p><p>I have had many interactions with Israeli soldiers and settlers on that street. It is a place filled with tension and injustice. But seeing people from across the world stand there and sing a song of resistance gave me hope. It showed that more people are becoming aware of the Palestinian struggle.</p><iframe height="300" src="https://www.youtube.com/embed/Zd-AKmjhJTw" style="width:100%;"></iframe><p>Much of what we know about the war on Gaza comes from brave journalists risking their lives to report the truth. Many of us around the world have followed them on Instagram, relying on their updates to understand what is happening on the ground. Here are some of the journalists whose work has made a difference: <a href="https://www.instagram.com/wael_eldahdouh/" target="__blank">@wael_eldahdouh</a> <a href="https://www.instagram.com/anasjamal44/" target="__blank">@anasjamal44</a> <a href="https://www.instagram.com/anas.jamal33/" target="__blank">@anas.jamal33</a> <a href="https://www.instagram.com/hossam_shbat/" target="__blank">@hossam_shbat</a></p><p>The struggle continues, and so does our resistance. One day, Palestine will be free.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;A long time ago, I was having a conversation with my mother about my country Palestine. I told her that the situation would get worse. She said, “This is our life. It is our destiny to live this struggle and fight it. For that, we were born in this land. This is our test.”&lt;/p&gt;</summary>
    
    
    
    <category term="diary" scheme="https://abozanona.me/categories/diary/"/>
    
    
  </entry>
  
  <entry>
    <title>Pacman contribution graph</title>
    <link href="https://abozanona.me/pacman-contribution-graph/"/>
    <id>https://abozanona.me/pacman-contribution-graph/</id>
    <published>2025-01-30T22:23:23.000Z</published>
    <updated>2026-06-05T23:01:43.337Z</updated>
    
    <content type="html"><![CDATA[<p>Have you ever looked at your GitLab or GitHub contribution graph and thought, <em>“This could be… more exciting?”</em> I know I have. I wanted a fun and creative way to visualize my contributions, especially on GitLab, where I store all my private projects. Unfortunately, I couldn’t find anything online that did the trick.</p><span id="more"></span><p>Then, I stumbled upon <strong>snk</strong> (check it out <a href="https://github.com/Platane/snk/">here</a>). It’s a super cool project that turns your GitHub contribution graph into a snake game. You simply provide your GitHub username, and it generates an image of a snake weaving its way through your contributions. Awesome, right? But there was one problem: <strong>snk only supports GitHub.</strong> Since I primarily use GitLab for private projects, I needed something similar, but there wasn’t a solution out there. So, I decided to create my own.</p><hr><h2 id="Introducing-the-Pac-Man-Contribution-Game-🎮"><a href="#Introducing-the-Pac-Man-Contribution-Game-🎮" class="headerlink" title="Introducing the Pac-Man Contribution Game 🎮"></a>Introducing the Pac-Man Contribution Game 🎮</h2><p>Instead of a snake, I thought, <em>why not Pac-Man?</em> Pac-Man is iconic, dynamic, and just plain fun. I developed a game that overlays a Pac-Man grid on your GitLab or GitHub contribution history. Here’s how it works:</p><ul><li>The game generates your contribution grid and populates it with <strong>Pac-Man and four ghosts</strong>. </li><li>Pac-Man’s mission? <strong>Clear all the cells</strong> on the grid. </li><li>The ghosts? <strong>Chasing Pac-Man</strong> to stop him!</li><li>If Pac-Man eats enough cells, he gets a power-up and can turn the tables on the ghosts, making them flee.</li><li>The game ends when Pac-Man clears the entire grid.</li></ul><p>It’s a fully automated experience — no user input required! Just sit back and watch as Pac-Man chomps his way through your contributions.  </p><hr><h2 id="How-It-Works"><a href="#How-It-Works" class="headerlink" title="How It Works"></a>How It Works</h2><p>The game pulls your commit history from <strong>GitHub</strong> or <strong>GitLab</strong> and translates it into a contribution grid. Then, it places Pac-Man and the ghosts on the grid. </p><p>There are <strong>two export options</strong> for the game:  </p><ol><li><strong>HTML Canvas</strong>: Perfect for running the game in your browser.  </li><li><strong>Animated SVG</strong>: Downloadable and sharable for use anywhere.</li></ol><p>You can try the game for yourself right now: <a href="https://abozanona.github.io/pacman-contribution-graph/">Pac-Man Contribution Game Demo</a>.  </p><p>You can also see how it looks like on Github profiles live on <a href="https://github.com/abozanona">@abozanona</a>.</p><p>This is how canvas export for my gitlab account looks like:</p><img src="https://gitlab.com/abozanona/abozanona/-/raw/main/output/pacman-contribution-graph-light.svg" style="width: 100%"/><hr><h2 id="What’s-Next"><a href="#What’s-Next" class="headerlink" title="What’s Next?"></a>What’s Next?</h2><p>Of course, the project isn’t perfect (yet). Here are a few things I’m working on improving:  </p><ul><li><strong>Smarter movement algorithms</strong>: Currently, the sprites use basic algorithms. I want them to behave more like the real Pac-Man game.  </li><li><strong>Better sprite design</strong>: Pac-Man’s mouth animation, ghost shapes, and collision effects could all use some love.  </li><li><strong>Fixing SVG issues</strong>: The exported SVG occasionally has glitches with sprite movement and design that need smoothing out. And Pac-Man never goes out of power-up mode; this results in a huge svg file size.</li></ul><p>I’m also planning to add some exciting new features, like the ability to integrate the game directly into your GitHub profile — similar to how the snk game works.  </p><hr><h2 id="Why-I-Built-This"><a href="#Why-I-Built-This" class="headerlink" title="Why I Built This"></a>Why I Built This</h2><p>At its core, this project is about creativity and having fun with your developer tools. Contribution graphs are a snapshot of your coding journey, and turning them into a playable game adds a unique twist. Whether you’re showcasing your coding streak or just having a laugh, I hope this Pac-Man game brings some joy to your day.</p><p>Check out the <a href="https://github.com/abozanona/pacman-contribution-graph/">project repository</a> for more details and to see how it works. Feedback, ideas, and contributions are always welcome!  </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Have you ever looked at your GitLab or GitHub contribution graph and thought, &lt;em&gt;“This could be… more exciting?”&lt;/em&gt; I know I have. I wanted a fun and creative way to visualize my contributions, especially on GitLab, where I store all my private projects. Unfortunately, I couldn’t find anything online that did the trick.&lt;/p&gt;</summary>
    
    
    
    <category term="project" scheme="https://abozanona.me/categories/project/"/>
    
    
  </entry>
  
  <entry>
    <title>Solving mobile video issues with `playsinline`</title>
    <link href="https://abozanona.me/solving-mobile-video-issues-with-playsinline/"/>
    <id>https://abozanona.me/solving-mobile-video-issues-with-playsinline/</id>
    <published>2025-01-07T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.733Z</updated>
    
    <content type="html"><![CDATA[<p>While working on a project that involved building a massive video library, I ran into an interesting issue on mobile devices. On certain pages, we had <strong>short video previews</strong> that auto-played as soon as the user navigated to the page. While it worked perfectly on desktop browsers, mobile browsers kept forcing the videos into fullscreen mode as soon as they started playing.</p><span id="more"></span><p>This wasn’t the experience we wanted. The goal was to keep the videos inline — playing directly on the page without taking over the entire screen. Another related issue, when users clicked on a video on mobile, it would also jump into fullscreen mode, disrupting the browsing flow.</p><p>After some research, I found the culprit: <strong>the missing <code>playsinline</code> attribute</strong>.</p><p>The <code>playsinline</code> attribute is a simple yet powerful property for HTML <code>&lt;video&gt;</code> elements. By default, mobile browsers often play videos in fullscreen mode to provide a better viewing experience on small screens. However, there are many cases — like my project — where you don’t want that behavior.</p><p>Adding the <code>playsinline</code> attribute tells the browser to play the video <strong>directly within the page</strong> (inline) instead of switching to fullscreen. Here’s what it looks like:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">video</span> <span class="attr">src</span>=<span class="string">&quot;example.mp4&quot;</span> <span class="attr">controls</span> <span class="attr">playsinline</span>&gt;</span><span class="tag">&lt;/<span class="name">video</span>&gt;</span></span><br></pre></td></tr></table></figure><p>With <code>playsinline</code> in place, the video stays embedded within the page, allowing users to watch it without breaking their browsing flow.</p><p>By adding this small but impactful attribute, I was able to resolve the fullscreen playback issue on mobile and create a smoother experience for our video library users.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;While working on a project that involved building a massive video library, I ran into an interesting issue on mobile devices. On certain pages, we had &lt;strong&gt;short video previews&lt;/strong&gt; that auto-played as soon as the user navigated to the page. While it worked perfectly on desktop browsers, mobile browsers kept forcing the videos into fullscreen mode as soon as they started playing.&lt;/p&gt;</summary>
    
    
    
    <category term="technical" scheme="https://abozanona.me/categories/technical/"/>
    
    
  </entry>
  
  <entry>
    <title>Reduce website load and render time</title>
    <link href="https://abozanona.me/reduce-website-load-and-render-time/"/>
    <id>https://abozanona.me/reduce-website-load-and-render-time/</id>
    <published>2024-11-23T22:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.732Z</updated>
    
    <content type="html"><![CDATA[<p>Since last August, I’ve been working on an exciting freelancing project to rebuild and optimize a long-running customer website. The original website had performance issues, with slow loading speeds affecting user experience. My goal was not just to rebuild but to significantly improve the speed and efficiency of the site. Here’s how I approached it.</p><span id="more"></span><h2 id="Starting-Fresh-with-a-Strong-Foundation"><a href="#Starting-Fresh-with-a-Strong-Foundation" class="headerlink" title="Starting Fresh with a Strong Foundation"></a>Starting Fresh with a Strong Foundation</h2><p>The first step was selecting a reliable framework. I chose a Vue.js Vuetify theme to rebuild the website, offering a modern design and a solid structure. After completing the initial build, I shifted my focus to aggressive optimization to ensure top-notch performance.</p><h2 id="Code-Refinement-and-Optimization"><a href="#Code-Refinement-and-Optimization" class="headerlink" title="Code Refinement and Optimization"></a>Code Refinement and Optimization</h2><p>A key part of improving speed was streamlining the codebase:</p><ol><li><p>Cleaning Up Components and Dependencies<br>I identified and removed unused components and unnecessary dependencies from the code and <code>package.json</code>, ensuring a lean and efficient setup.</p></li><li><p>Strict Code Quality Rules<br>Enforcing strict linting rules in <code>.eslintrc</code> helped catch and prevent issues like undefined or unimported components.</p></li><li><p>Bundle Analysis and Reduction<br>Using my <code>&quot;bundle-visualize&quot;</code> command (<code>npx vite-bundle-visualizer</code>), I identified large, unused modules in the bundle. Removing them reduced the overall bundle size and optimized load times.</p></li><li><p>Minification of CSS and JS<br>Enabling CSS and JavaScript minification in the build process further reduced file sizes, cutting down the data sent to users.</p></li></ol><h2 id="Addressing-CSS-Bottlenecks"><a href="#Addressing-CSS-Bottlenecks" class="headerlink" title="Addressing CSS Bottlenecks"></a>Addressing CSS Bottlenecks</h2><p>One major hurdle was the size of the bundled CSS file. During analysis, I found several opportunities for improvement:</p><ul><li><p>Optimizing Material Design Icons (MDI)<br>By default, all MDI icons were included in the CSS file. Importing only the icons used in the project and loading them in SVG format significantly reduced the file size.</p></li><li><p>Simplifying Component Usage<br>I avoided Vuetify components with overly complex functionality and styles, instead opting for simpler custom components that better fit the project’s needs.</p></li><li><p>Dynamic CSS Loading<br>CSS files are now loaded dynamically for components only when they are rendered. This split approach ensures smaller, more manageable CSS files while improving rendering performance.</p></li></ul><h2 id="Optimizing-Images"><a href="#Optimizing-Images" class="headerlink" title="Optimizing Images"></a>Optimizing Images</h2><p>Images often account for a large portion of page load times, so I implemented the following strategies:</p><ul><li><p>WebP Conversion and Compression<br>Images uploaded by users are compressed and converted to WebP format in a separate thread, reducing their size without compromising quality.</p></li><li><p>Lazy Loading<br>For image-heavy pages, lazy loading ensures that images are loaded only when they appear on the screen, improving initial load times.</p></li><li><p>Leveraging a CDN<br>All images are served through a CDN, ensuring faster delivery to users regardless of their geographic location.</p></li></ul><p>Additionally, when migrating all images from the old website to the new one, I compressed them and converted them to WebP. The customer was impressed by the enhanced user experience during testing.</p><h2 id="Server-Level-Enhancements"><a href="#Server-Level-Enhancements" class="headerlink" title="Server-Level Enhancements"></a>Server-Level Enhancements</h2><p>To support the front-end improvements, I optimized the server setup:</p><ul><li><p>Gzip Compression<br>Configuring gzip compression on the nginx server ensured that transferred files were compressed, speeding up delivery.</p></li><li><p>Effective Caching<br>Proper caching mechanisms for objects(redis), assets(cache time) and pages(zgip) minimized redundant data transfer and improved repeat visits’ speed.</p></li></ul><h2 id="Splitting-Applications-and-Resources"><a href="#Splitting-Applications-and-Resources" class="headerlink" title="Splitting Applications and Resources"></a>Splitting Applications and Resources</h2><p>The project involved three distinct applications, which initially shared a single JavaScript file. I split the Vue application into three separate modules, ensuring that each application loaded only the libraries and resources it needed. This significantly reduced unnecessary resource loading across the board.</p><h2 id="Impressive-Results-and-Future-Plans"><a href="#Impressive-Results-and-Future-Plans" class="headerlink" title="Impressive Results and Future Plans"></a>Impressive Results and Future Plans</h2><p>Applying these strategies has drastically reduced the website’s loading time compared to the old version. The final CSS and JavaScript bundles are much smaller, and the split CSS files have made a noticeable difference in performance.</p><p>However, the optimization journey doesn’t stop here. I plan to explore additional techniques to further improve loading speed in the future. This project has been a rewarding challenge, blending technical problem-solving with creative thinking. I’m excited to see how users respond to the improved experience when the site launches in February.  </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Since last August, I’ve been working on an exciting freelancing project to rebuild and optimize a long-running customer website. The original website had performance issues, with slow loading speeds affecting user experience. My goal was not just to rebuild but to significantly improve the speed and efficiency of the site. Here’s how I approached it.&lt;/p&gt;</summary>
    
    
    
    <category term="project" scheme="https://abozanona.me/categories/project/"/>
    
    
  </entry>
  
  <entry>
    <title>Implement Basic Authentication Using Cloudflare Workers</title>
    <link href="https://abozanona.me/implement-basic-authentication-using-cloudflare-workers/"/>
    <id>https://abozanona.me/implement-basic-authentication-using-cloudflare-workers/</id>
    <published>2024-10-15T21:23:23.000Z</published>
    <updated>2025-10-01T23:34:03.730Z</updated>
    
    <content type="html"><![CDATA[<p>When I started working on migrating data from the old website of a project I’m developing for a client to the new website, I felt the need to prevent external users from accessing the site and seeing the migrated data. Only internal users should have access during this phase.  </p><p>Typically, I implement Basic Authentication through nginx site configurations, which works well. However, I recalled reading that this could also be achieved with Cloudflare Workers—offering a simpler and more flexible solution. In this post, I’ll guide you through implementing Basic Authentication using Cloudflare Workers.  </p><span id="more"></span><h2 id="What-Is-Basic-Authentication-and-Why-Use-It"><a href="#What-Is-Basic-Authentication-and-Why-Use-It" class="headerlink" title="What Is Basic Authentication and Why Use It?"></a>What Is Basic Authentication and Why Use It?</h2><p>Basic Authentication is a simple way to secure access to web resources by requiring a username and password. It’s particularly useful during internal testing or development phases when you want to restrict access to specific users without setting up a full-fledged authentication system.  </p><p>Some advantages include:  </p><ul><li>Quick Setup: Easy to configure without additional software or database dependencies.  </li><li>Ideal for Testing: Ensures only internal testers can access the website or specific resources.  </li><li>Lightweight: Minimal performance impact compared to more complex authentication mechanisms.</li></ul><h2 id="How-to-Integrate-Basic-Authentication-with-Cloudflare"><a href="#How-to-Integrate-Basic-Authentication-with-Cloudflare" class="headerlink" title="How to Integrate Basic Authentication with Cloudflare"></a>How to Integrate Basic Authentication with Cloudflare</h2><p>Turns out, it’s a straightforward process. Here’s how:  </p><ol><li><p>Navigate to Cloudflare Workers<br>In the Cloudflare dashboard, go to <code>Workers &amp; Pages &gt; Overview</code> from the left sidebar and create a new worker.  </p></li><li><p>Add the Code<br>Copy and paste the following code into your worker script, then deploy it:</p></li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="variable constant_">CREDENTIALS</span> = &#123;</span><br><span class="line">  <span class="attr">username</span>: <span class="string">&#x27;internal_tester&#x27;</span>,</span><br><span class="line">  <span class="attr">password</span>: <span class="string">&#x27;$t7on8_p@@@w6&#x27;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Main entry point</span></span><br><span class="line"><span class="title function_">addEventListener</span>(<span class="string">&#x27;fetch&#x27;</span>, <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">  event.<span class="title function_">respondWith</span>(<span class="title function_">handleRequest</span>(event.<span class="property">request</span>));</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">handleRequest</span>(<span class="params">request</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> authorizationHeader = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&#x27;authorization&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!authorizationHeader) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">unauthorizedResponse</span>(<span class="string">&#x27;Missing authorization.&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> [providedUsername, providedPassword] = <span class="title function_">extractCredentials</span>(authorizationHeader);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isValidUser</span>(providedUsername, providedPassword)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">unauthorizedResponse</span>(<span class="string">&#x27;Invalid username/password.&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">fetch</span>(request);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Extract username and password from the Authorization header</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">extractCredentials</span>(<span class="params">authorizationHeader</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [type, encodedCredentials] = authorizationHeader.<span class="title function_">split</span>(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (type !== <span class="string">&#x27;Basic&#x27;</span> || !encodedCredentials) &#123;</span><br><span class="line">    <span class="keyword">return</span> [<span class="string">&#x27;&#x27;</span>, <span class="string">&#x27;&#x27;</span>];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> decodedCredentials = <span class="title function_">atob</span>(encodedCredentials);</span><br><span class="line">  <span class="keyword">return</span> decodedCredentials.<span class="title function_">split</span>(<span class="string">&#x27;:&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Check if the provided username and password are valid</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isValidUser</span>(<span class="params">username, password</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> username === <span class="variable constant_">CREDENTIALS</span>.<span class="property">username</span> &amp;&amp; password === <span class="variable constant_">CREDENTIALS</span>.<span class="property">password</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Generate a 401 Unauthorized response</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">unauthorizedResponse</span>(<span class="params">message</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">new</span> <span class="title class_">Response</span>(message, &#123; <span class="attr">status</span>: <span class="number">401</span> &#125;);</span><br><span class="line">  response.<span class="property">headers</span>.<span class="title function_">set</span>(<span class="string">&#x27;WWW-Authenticate&#x27;</span>, <span class="string">&#x27;Basic realm=&quot;Secure Area&quot;&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> response;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Explanation-of-the-Code"><a href="#Explanation-of-the-Code" class="headerlink" title="Explanation of the Code"></a>Explanation of the Code</h2><ul><li>Static Credentials: The <code>CREDENTIALS</code> object contains the username and password. This can be adjusted as needed.  </li><li>Request Handling:  <ul><li>Checks for the <code>Authorization</code> header in the incoming request.  </li><li>If the header is missing or credentials are invalid, it responds with a <code>401 Unauthorized</code> status.  </li><li>If valid, it fetches and serves the original request.</li></ul></li><li>Response Headers: The <code>WWW-Authenticate</code> header prompts the browser to display a login dialog when authentication is required.</li></ul><h2 id="Deployment"><a href="#Deployment" class="headerlink" title="Deployment"></a>Deployment</h2><p>After deploying the worker, you can attach it to your domain or specific route where Basic Authentication is needed. Now, only users with the correct credentials will have access to the secured pages.  </p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>With just a few lines of JavaScript and Cloudflare Workers, you can implement a lightweight and effective Basic Authentication mechanism. This is ideal for securing your site during development or testing phases without complicating the process.  </p><p>And voilà! Your Basic Authentication is ready for internal testers to use.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;When I started working on migrating data from the old website of a project I’m developing for a client to the new website, I felt the need to prevent external users from accessing the site and seeing the migrated data. Only internal users should have access during this phase.  &lt;/p&gt;
&lt;p&gt;Typically, I implement Basic Authentication through nginx site configurations, which works well. However, I recalled reading that this could also be achieved with Cloudflare Workers—offering a simpler and more flexible solution. In this post, I’ll guide you through implementing Basic Authentication using Cloudflare Workers.  &lt;/p&gt;</summary>
    
    
    
    <category term="project" scheme="https://abozanona.me/categories/project/"/>
    
    
  </entry>
  
</feed>
