[{"content":"I\u0026rsquo;ve been setting up a self-hosted CI pipeline — Jenkins running in Docker on a QNAP NAS, tunneled out to GitHub via Cloudflare. It\u0026rsquo;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.\nI decided to do most of it inside Cursor using Claude as the AI backend, leaning into what people are calling \u0026ldquo;agentic\u0026rdquo; 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\u0026rsquo;s what I noticed.\nWhat\u0026rsquo;s different about agentic flow Normal AI-assisted coding looks like this: you ask a question, get an answer, copy something, ask a follow-up, repeat. It\u0026rsquo;s useful but you\u0026rsquo;re still doing most of the driving.\nAgentic flow is different. You describe what you\u0026rsquo;re trying to accomplish — \u0026ldquo;set up a Jenkins pipeline that builds an Android APK, runs Espresso tests, and passes the result to MobSF for a security scan\u0026rdquo; — and the model starts working through it. It reads your existing files, figures out what\u0026rsquo;s missing, writes what\u0026rsquo;s needed, and checks its own output. You\u0026rsquo;re more like a reviewer than a driver.\nThe 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\u0026rsquo;t need me to repeat myself.\nWhat it was actually like for the QNAP setup The CI pipeline involved a few distinct problems that would normally each require separate research sessions:\nDocker Compose on QNAP — 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.\nThe Jenkinsfile — 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\u0026rsquo;t have to look at the Jenkins documentation once.\nMobSF integration — 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\u0026rsquo;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.\nThe Cloudflare Tunnel piece was probably the easiest because the concept is straightforward once you understand it, and Claude explained the \u0026ldquo;why\u0026rdquo; well — outbound connection from your machine, no ports, permanent URL — which made it easier to debug when something wasn\u0026rsquo;t routing correctly.\nWhere it still needs you It\u0026rsquo;s not a replacement for understanding what you\u0026rsquo;re building. A few times Claude produced something that looked right but had an assumption baked in that didn\u0026rsquo;t match my setup. If I hadn\u0026rsquo;t known enough to catch it, I would have had a broken pipeline and no idea why.\nIt also doesn\u0026rsquo;t know what it doesn\u0026rsquo;t know. If you\u0026rsquo;re exploring something genuinely novel or obscure, it\u0026rsquo;ll sometimes fill in gaps with plausible-sounding things that aren\u0026rsquo;t quite right. You still need to verify.\nAnd 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.\nWould I use it this way again 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.\nHaving something that can hold the full context of what you\u0026rsquo;re building, write the boilerplate confidently, and explain its reasoning as it goes makes the whole thing faster and less frustrating. It doesn\u0026rsquo;t replace the thinking — you still have to know what you want and whether you\u0026rsquo;re getting it — but it removes a lot of the friction in between.\nFor 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.\n","permalink":"https://faldayani.us/blog/cursor-claude-agentic-flow/","summary":"I used Cursor and the Claude API to help build a self-hosted CI pipeline on a QNAP NAS. Here\u0026rsquo;s how it changed the way I approached the work.","title":"Trying Cursor with Claude's Agentic Flow"},{"content":"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.\nThe short version: it works. Here\u0026rsquo;s how it\u0026rsquo;s set up.\nThe stack QNAP NAS — the machine everything runs on Container Station — QNAP\u0026rsquo;s Docker management UI, used to run Jenkins and the other containers Jenkins — the CI orchestrator that watches for PRs and runs the pipeline Gradle — builds the APK inside the Jenkins pipeline Espresso — Android UI tests that run as part of the build MobSF — a static security scanner that inspects the APK before it\u0026rsquo;s allowed through Cloudflare Tunnel — how GitHub reaches Jenkins without any port forwarding The problem with running CI at home 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\u0026rsquo;t have a public IP address, and even if it did, you\u0026rsquo;d have to open ports and deal with a dynamic IP changing on you.\nCloudflare Tunnel solves this cleanly. You run a small agent (cloudflared) on the QNAP as another Docker container. It opens an outbound connection to Cloudflare\u0026rsquo;s edge and keeps it alive. Cloudflare gives you a permanent public URL — something like https://your-tunnel.cfargotunnel.com — and any traffic hitting that URL gets forwarded through the tunnel to Jenkins running locally. No ports opened. No domain purchase. No IP management.\nFrom GitHub\u0026rsquo;s perspective, Jenkins just looks like any other server on the internet.\nThe pipeline When a pull request targets main, GitHub fires a webhook to Jenkins. The pipeline runs three stages in order:\n1. Build\nJenkins checks out the branch and runs the Gradle build inside the container:\n./gradlew assembleDebug If the build fails — compile error, missing dependency, whatever — the pipeline stops here and the PR is blocked.\n2. UI Tests\nEspresso tests run against the debug APK. This is the stage that catches things a unit test wouldn\u0026rsquo;t — navigation flows, UI state after an action, things that only break when the full app is running.\n./gradlew connectedDebugAndroidTest 3. Security Scan\nThe APK gets passed to MobSF, which does a static analysis — checking for hardcoded secrets, insecure API usage, exported components that shouldn\u0026rsquo;t be, and a few dozen other things. If MobSF finds anything above the severity threshold, the stage fails.\nAll three stages have to pass for the PR to be mergeable. GitHub\u0026rsquo;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.\nWhy this setup The main reason is cost. Once it\u0026rsquo;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\u0026rsquo;re spending is electricity and the time it takes to set it up.\nThe second reason is that it\u0026rsquo;s genuinely interesting to build. Running a build system on a NAS, tunneling it through Cloudflare\u0026rsquo;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.\nWhat\u0026rsquo;s left 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 main can go straight to a draft Play Store release.\nMore on those as they come together.\n","permalink":"https://faldayani.us/blog/self-hosted-android-ci-qnap/","summary":"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.","title":"Self-Hosted Android CI on a QNAP NAS"},{"content":"I\u0026rsquo;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\u0026rsquo;d normally see in Jira, but without the $20/month and the 47 tabs it takes to do anything.\nThe 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\u0026rsquo;s how it works.\nThe setup Inside Notion I have a project board with three databases:\nEpics — the big feature areas (e.g. \u0026ldquo;Blog Section\u0026rdquo;, \u0026ldquo;Mobile App v2\u0026rdquo;) Issues — the actual tasks, linked back to an epic Sprints — time boxes that issues can be assigned to Issues have all the fields you\u0026rsquo;d expect: status, priority, labels, story points, assignee, due date. Epics and sprints have their own trimmed-down schemas.\nTo 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\u0026rsquo;re just making HTTP requests.\nCreating an epic Here\u0026rsquo;s what creating an epic looks like at the API level:\ncurl -X POST \u0026#34;https://api.notion.com/v1/pages\u0026#34; \\ -H \u0026#34;Authorization: Bearer $NOTION_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Notion-Version: 2022-06-28\u0026#34; \\ -d \u0026#39;{ \u0026#34;parent\u0026#34;: { \u0026#34;database_id\u0026#34;: \u0026#34;your-epics-db-id\u0026#34; }, \u0026#34;properties\u0026#34;: { \u0026#34;Name\u0026#34;: { \u0026#34;title\u0026#34;: [{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;Blog Section\u0026#34; } }] }, \u0026#34;Status\u0026#34;: { \u0026#34;select\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Backlog\u0026#34; } }, \u0026#34;Priority\u0026#34;: { \u0026#34;select\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Medium\u0026#34; } } } }\u0026#39; You\u0026rsquo;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\u0026rsquo;re making an epic, an issue, or a sprint.\nCreating issues under that epic 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\u0026rsquo;s page ID, not the database ID — I got tripped up on that the first time.\ncurl -X POST \u0026#34;https://api.notion.com/v1/pages\u0026#34; \\ -H \u0026#34;Authorization: Bearer $NOTION_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Notion-Version: 2022-06-28\u0026#34; \\ -d \u0026#39;{ \u0026#34;parent\u0026#34;: { \u0026#34;database_id\u0026#34;: \u0026#34;your-issues-db-id\u0026#34; }, \u0026#34;properties\u0026#34;: { \u0026#34;Name\u0026#34;: { \u0026#34;title\u0026#34;: [{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;Add blog section to Hugo site\u0026#34; } }] }, \u0026#34;Status\u0026#34;: { \u0026#34;select\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;To Do\u0026#34; } }, \u0026#34;Priority\u0026#34;: { \u0026#34;select\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;High\u0026#34; } }, \u0026#34;Labels\u0026#34;: { \u0026#34;multi_select\u0026#34;: [{ \u0026#34;name\u0026#34;: \u0026#34;Frontend\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;Feature\u0026#34; }] }, \u0026#34;Epic\u0026#34;: { \u0026#34;relation\u0026#34;: [{ \u0026#34;id\u0026#34;: \u0026#34;epic-page-id-here\u0026#34; }] } } }\u0026#39; Moving an issue through the board Updating status is just a PATCH to the page with the new select value:\ncurl -X PATCH \u0026#34;https://api.notion.com/v1/pages/issue-page-id\u0026#34; \\ -H \u0026#34;Authorization: Bearer $NOTION_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Notion-Version: 2022-06-28\u0026#34; \\ -d \u0026#39;{ \u0026#34;properties\u0026#34;: { \u0026#34;Status\u0026#34;: { \u0026#34;select\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Done\u0026#34; } } } }\u0026#39; Wrapping it in a CLI Typing all that curl by hand gets old fast. I wrote a small Node.js script (notion.js) that wraps the common operations so I can do things like:\n# Create an epic node notion.js create-epic --title \u0026#34;Blog Section\u0026#34; --priority Medium # Create an issue under it node notion.js create-issue --title \u0026#34;Deploy to production\u0026#34; --epic \u0026#34;Blog Section\u0026#34; --priority High --labels \u0026#34;Feature\u0026#34; # See what\u0026#39;s open node notion.js list-issues --status \u0026#34;In Progress\u0026#34; # Close it out node notion.js move-issue --id page-id-here --status \u0026#34;Done\u0026#34; The script finds the epic by name, looks up its page ID, and wires everything together — so I never have to paste IDs manually.\nWhy bother 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\u0026rsquo;m already working in.\nIt 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.\nNot something I\u0026rsquo;d recommend for a team, but for solo projects it\u0026rsquo;s a nice middle ground between \u0026ldquo;sticky note on my monitor\u0026rdquo; and a full Jira setup.\n","permalink":"https://faldayani.us/blog/hello-world/","summary":"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.","title":"Using Notion as My Personal Jira"},{"content":"A minimal Android notes app built with Kotlin. Supports notes, todos, tags, dark mode, and custom color themes.\nView on GitHub\n","permalink":"https://faldayani.us/projects/yellow-note/","summary":"\u003cp\u003eA minimal Android notes app built with Kotlin. Supports notes, todos, tags, dark mode, and custom color themes.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/faldayani/yellownote\"\u003eView on GitHub\u003c/a\u003e\u003c/p\u003e","title":"Yellow Note"},{"content":"Hi, I\u0026rsquo;m Faisal — a cloud engineer based in Texas.\nI build mobile apps, tools, and other things I find interesting. You can find my work on GitHub and connect with me on LinkedIn.\nFeel free to reach out at faisal.aldayani@gmail.com.\n","permalink":"https://faldayani.us/about/","summary":"about","title":"About"}]