<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Blog on Faisal Al-Dayani</title><link>https://faldayani.us/blog/</link><description>Recent content in Blog on Faisal Al-Dayani</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 25 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://faldayani.us/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Trying Cursor with Claude's Agentic Flow</title><link>https://faldayani.us/blog/cursor-claude-agentic-flow/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://faldayani.us/blog/cursor-claude-agentic-flow/</guid><description>I used Cursor and the Claude API to help build a self-hosted CI pipeline on a QNAP NAS. Here&amp;rsquo;s how it changed the way I approached the work.</description><content:encoded>&lt;![CDATA[<p>I&rsquo;ve been setting up a self-hosted CI pipeline — Jenkins running in Docker on a QNAP NAS, tunneled out to GitHub via Cloudflare. It&rsquo;s not a trivial project. There are a lot of moving pieces: Docker Compose configs, Jenkinsfiles, webhook setup, network routing, MobSF integration. Normally this is the kind of thing where you spend a lot of time jumping between documentation tabs and Stack Overflow, stitching things together slowly.</p><p>I decided to do most of it inside Cursor using Claude as the AI backend, leaning into what people are calling &ldquo;agentic&rdquo; mode — where instead of asking one question at a time, you describe a goal and let the AI work through it across multiple steps. Here&rsquo;s what I noticed.</p><h2 id="whats-different-about-agentic-flow">What&rsquo;s different about agentic flow</h2><p>Normal AI-assisted coding looks like this: you ask a question, get an answer, copy something, ask a follow-up, repeat. It&rsquo;s useful but you&rsquo;re still doing most of the driving.</p><p>Agentic flow is different. You describe what you&rsquo;re trying to accomplish — &ldquo;set up a Jenkins pipeline that builds an Android APK, runs Espresso tests, and passes the result to MobSF for a security scan&rdquo; — and the model starts working through it. It reads your existing files, figures out what&rsquo;s missing, writes what&rsquo;s needed, and checks its own output. You&rsquo;re more like a reviewer than a driver.</p><p>The thing that surprised me is how much context it holds across the work. When I was setting up the Cloudflare Tunnel container, it already knew from earlier in the session how Jenkins was configured, which port it was on, and what the Docker network was called. It didn&rsquo;t need me to repeat myself.</p><h2 id="what-it-was-actually-like-for-the-qnap-setup">What it was actually like for the QNAP setup</h2><p>The CI pipeline involved a few distinct problems that would normally each require separate research sessions:</p><p><strong>Docker Compose on QNAP</strong> — Container Station has some quirks compared to a standard Docker host. Getting the volume mounts right for the Android SDK inside the Jenkins container took some back and forth, but Claude flagged the QNAP-specific paths without me having to look them up.</p><p><strong>The Jenkinsfile</strong> — Writing a declarative pipeline with three stages (build, test, security scan) and the right failure conditions is the kind of thing where the syntax is fiddly and easy to get wrong. Claude wrote a working first draft, I told it what I wanted to change, and it updated it. I didn&rsquo;t have to look at the Jenkins documentation once.</p><p><strong>MobSF integration</strong> — This was the part I was least sure about. MobSF has an API, but wiring it into a Jenkins stage — uploading the APK, polling for the scan result, failing the build above a certain severity score — is not something I&rsquo;d done before. Claude worked through the API calls, wrote the shell script that handles the integration, and explained what each part was doing as it went.</p><p>The Cloudflare Tunnel piece was probably the easiest because the concept is straightforward once you understand it, and Claude explained the &ldquo;why&rdquo; well — outbound connection from your machine, no ports, permanent URL — which made it easier to debug when something wasn&rsquo;t routing correctly.</p><h2 id="where-it-still-needs-you">Where it still needs you</h2><p>It&rsquo;s not a replacement for understanding what you&rsquo;re building. A few times Claude produced something that looked right but had an assumption baked in that didn&rsquo;t match my setup. If I hadn&rsquo;t known enough to catch it, I would have had a broken pipeline and no idea why.</p><p>It also doesn&rsquo;t know what it doesn&rsquo;t know. If you&rsquo;re exploring something genuinely novel or obscure, it&rsquo;ll sometimes fill in gaps with plausible-sounding things that aren&rsquo;t quite right. You still need to verify.</p><p>And the agentic mode works best when you give it a clear target. Vague prompts produce vague results. The more specific you are about what done looks like, the better the output.</p><h2 id="would-i-use-it-this-way-again">Would I use it this way again</h2><p>Yes, without hesitation. The CI pipeline would have taken me significantly longer to piece together manually — not because any individual part is that hard, but because there are a lot of parts, and keeping them all in your head while also looking things up gets tiring fast.</p><p>Having something that can hold the full context of what you&rsquo;re building, write the boilerplate confidently, and explain its reasoning as it goes makes the whole thing faster and less frustrating. It doesn&rsquo;t replace the thinking — you still have to know what you want and whether you&rsquo;re getting it — but it removes a lot of the friction in between.</p><p>For infrastructure work especially, where the reward is a thing that runs quietly in the background and never needs attention again, that trade-off feels very worth it.</p>
]]></content:encoded></item><item><title>Self-Hosted Android CI on a QNAP NAS</title><link>https://faldayani.us/blog/self-hosted-android-ci-qnap/</link><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><guid>https://faldayani.us/blog/self-hosted-android-ci-qnap/</guid><description>How I built a production-style CI pipeline for an Android app using Jenkins, Docker, and a Cloudflare Tunnel — running entirely on a NAS I already own.</description><content:encoded>&lt;![CDATA[<p>Most Android CI setups end up on GitHub Actions or Bitrise or some other cloud runner. They work fine, but once your free minutes run out or your team grows, the bill follows. I wanted to see if I could get the same result — automated builds, UI tests, security scans — on hardware I already had sitting on my desk.</p><p>The short version: it works. Here&rsquo;s how it&rsquo;s set up.</p><h2 id="the-stack">The stack</h2><ul><li><strong>QNAP NAS</strong> — the machine everything runs on</li><li><strong>Container Station</strong> — QNAP&rsquo;s Docker management UI, used to run Jenkins and the other containers</li><li><strong>Jenkins</strong> — the CI orchestrator that watches for PRs and runs the pipeline</li><li><strong>Gradle</strong> — builds the APK inside the Jenkins pipeline</li><li><strong>Espresso</strong> — Android UI tests that run as part of the build</li><li><strong>MobSF</strong> — a static security scanner that inspects the APK before it&rsquo;s allowed through</li><li><strong>Cloudflare Tunnel</strong> — how GitHub reaches Jenkins without any port forwarding</li></ul><h2 id="the-problem-with-running-ci-at-home">The problem with running CI at home</h2><p>The obvious issue with self-hosted CI is that your machine is behind a router at home. GitHub needs to send webhook events to Jenkins when a PR is opened — but your NAS doesn&rsquo;t have a public IP address, and even if it did, you&rsquo;d have to open ports and deal with a dynamic IP changing on you.</p><p>Cloudflare Tunnel solves this cleanly. You run a small agent (<code>cloudflared</code>) on the QNAP as another Docker container. It opens an outbound connection to Cloudflare&rsquo;s edge and keeps it alive. Cloudflare gives you a permanent public URL — something like<code>https://your-tunnel.cfargotunnel.com</code> — and any traffic hitting that URL gets forwarded through the tunnel to Jenkins running locally. No ports opened. No domain purchase. No IP management.</p><p>From GitHub&rsquo;s perspective, Jenkins just looks like any other server on the internet.</p><h2 id="the-pipeline">The pipeline</h2><p>When a pull request targets<code>main</code>, GitHub fires a webhook to Jenkins. The pipeline runs three stages in order:</p><p><strong>1. Build</strong></p><p>Jenkins checks out the branch and runs the Gradle build inside the container:</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./gradlew assembleDebug</span></span></code></pre></div><p>If the build fails — compile error, missing dependency, whatever — the pipeline stops here and the PR is blocked.</p><p><strong>2. UI Tests</strong></p><p>Espresso tests run against the debug APK. This is the stage that catches things a unit test wouldn&rsquo;t — navigation flows, UI state after an action, things that only break when the full app is running.</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./gradlew connectedDebugAndroidTest</span></span></code></pre></div><p><strong>3. Security Scan</strong></p><p>The APK gets passed to MobSF, which does a static analysis — checking for hardcoded secrets, insecure API usage, exported components that shouldn&rsquo;t be, and a few dozen other things. If MobSF finds anything above the severity threshold, the stage fails.</p><p>All three stages have to pass for the PR to be mergeable. GitHub&rsquo;s branch protection rules enforce this — the Jenkins job reports its status back to the PR, and the merge button stays locked until the check goes green.</p><h2 id="why-this-setup">Why this setup</h2><p>The main reason is cost. Once it&rsquo;s running, this pipeline costs nothing to operate. The NAS was already there. Cloudflare Tunnel is free. Jenkins is open source. The only thing you&rsquo;re spending is electricity and the time it takes to set it up.</p><p>The second reason is that it&rsquo;s genuinely interesting to build. Running a build system on a NAS, tunneling it through Cloudflare&rsquo;s edge, wiring up GitHub webhooks — there are a few moving parts that have to fit together, and getting there teaches you something about how CI infrastructure actually works, rather than just filling in a YAML file and hoping for the best.</p><h2 id="whats-left">What&rsquo;s left</h2><p>The pipeline is functional but there are a few things I still want to add — artifact storage so built APKs are retained, Slack notifications when a build fails, and eventually wiring up the release signing so a passing build on<code>main</code> can go straight to a draft Play Store release.</p><p>More on those as they come together.</p>
]]></content:encoded></item><item><title>Using Notion as My Personal Jira</title><link>https://faldayani.us/blog/hello-world/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://faldayani.us/blog/hello-world/</guid><description>How I set up Notion as a project tracker and wired it up to an API so I can create and update tasks from the terminal.</description><content:encoded>&lt;![CDATA[<p>I&rsquo;ve been using Notion for a while mostly as a notes app, but recently I set it up more like a proper project tracker — epics, issues, priorities, statuses, the whole thing. The kind of setup you&rsquo;d normally see in Jira, but without the $20/month and the 47 tabs it takes to do anything.</p><p>The part I actually enjoyed was connecting it to the Notion API so I can manage tasks from the terminal instead of clicking around in the UI. Here&rsquo;s how it works.</p><h2 id="the-setup">The setup</h2><p>Inside Notion I have a project board with three databases:</p><ul><li><strong>Epics</strong> — the big feature areas (e.g. &ldquo;Blog Section&rdquo;, &ldquo;Mobile App v2&rdquo;)</li><li><strong>Issues</strong> — the actual tasks, linked back to an epic</li><li><strong>Sprints</strong> — time boxes that issues can be assigned to</li></ul><p>Issues have all the fields you&rsquo;d expect: status, priority, labels, story points, assignee, due date. Epics and sprints have their own trimmed-down schemas.</p><p>To talk to all of this from outside Notion, you create an internal integration in your workspace settings, grab a token, and share the relevant pages with it. After that you&rsquo;re just making HTTP requests.</p><h2 id="creating-an-epic">Creating an epic</h2><p>Here&rsquo;s what creating an epic looks like at the API level:</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -X POST<span style="color:#e6db74">"https://api.notion.com/v1/pages"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Authorization: Bearer</span>$NOTION_TOKEN<span style="color:#e6db74">"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Content-Type: application/json"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Notion-Version: 2022-06-28"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -d<span style="color:#e6db74">'{</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "parent": { "database_id": "your-epics-db-id" },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "properties": {</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Name": { "title": [{ "type": "text", "text": { "content": "Blog Section" } }] },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Status": { "select": { "name": "Backlog" } },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Priority": { "select": { "name": "Medium" } }</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }'</span></span></span></code></pre></div><p>You&rsquo;re just creating a new page inside the Epics database. Notion treats every row in a database as a page, so the API is the same regardless of whether you&rsquo;re making an epic, an issue, or a sprint.</p><h2 id="creating-issues-under-that-epic">Creating issues under that epic</h2><p>Issues work the same way, but you also pass a relation back to the epic. The thing to know here is that relations use the epic row&rsquo;s<em>page ID</em>, not the database ID — I got tripped up on that the first time.</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -X POST<span style="color:#e6db74">"https://api.notion.com/v1/pages"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Authorization: Bearer</span>$NOTION_TOKEN<span style="color:#e6db74">"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Content-Type: application/json"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Notion-Version: 2022-06-28"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -d<span style="color:#e6db74">'{</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "parent": { "database_id": "your-issues-db-id" },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "properties": {</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Name": { "title": [{ "type": "text", "text": { "content": "Add blog section to Hugo site" } }] },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Status": { "select": { "name": "To Do" } },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Priority": { "select": { "name": "High" } },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Labels": { "multi_select": [{ "name": "Frontend" }, { "name": "Feature" }] },</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> "Epic": { "relation": [{ "id": "epic-page-id-here" }] }</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }'</span></span></span></code></pre></div><h2 id="moving-an-issue-through-the-board">Moving an issue through the board</h2><p>Updating status is just a PATCH to the page with the new select value:</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -X PATCH<span style="color:#e6db74">"https://api.notion.com/v1/pages/issue-page-id"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Authorization: Bearer</span>$NOTION_TOKEN<span style="color:#e6db74">"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Content-Type: application/json"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -H<span style="color:#e6db74">"Notion-Version: 2022-06-28"</span><span style="color:#ae81ff">\</span></span></span><span style="display:flex;"><span> -d<span style="color:#e6db74">'{ "properties": { "Status": { "select": { "name": "Done" } } } }'</span></span></span></code></pre></div><h2 id="wrapping-it-in-a-cli">Wrapping it in a CLI</h2><p>Typing all that curl by hand gets old fast. I wrote a small Node.js script (<code>notion.js</code>) that wraps the common operations so I can do things like:</p><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create an epic</span></span></span><span style="display:flex;"><span>node notion.js create-epic --title<span style="color:#e6db74">"Blog Section"</span> --priority Medium</span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#75715e"># Create an issue under it</span></span></span><span style="display:flex;"><span>node notion.js create-issue --title<span style="color:#e6db74">"Deploy to production"</span> --epic<span style="color:#e6db74">"Blog Section"</span> --priority High --labels<span style="color:#e6db74">"Feature"</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#75715e"># See what's open</span></span></span><span style="display:flex;"><span>node notion.js list-issues --status<span style="color:#e6db74">"In Progress"</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#75715e"># Close it out</span></span></span><span style="display:flex;"><span>node notion.js move-issue --id page-id-here --status<span style="color:#e6db74">"Done"</span></span></span></code></pre></div><p>The script finds the epic by name, looks up its page ID, and wires everything together — so I never have to paste IDs manually.</p><h2 id="why-bother">Why bother</h2><p>The main reason is that I find it easier to stay on top of side projects when the tracking is close to where the work is actually happening. Instead of switching to a browser and hunting for the right page, I can run a command in the same terminal I&rsquo;m already working in.</p><p>It also means I can have Claude update the board automatically as tasks finish — writing notes on what was done, any errors that came up, and how things got resolved. So the board stays accurate without extra effort on my end.</p><p>Not something I&rsquo;d recommend for a team, but for solo projects it&rsquo;s a nice middle ground between &ldquo;sticky note on my monitor&rdquo; and a full Jira setup.</p>
]]></content:encoded></item></channel></rss>