<?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: Pavel Polívka</title>
    <description>The latest articles on DEV Community by Pavel Polívka (@polivkadev).</description>
    <link>https://dev.to/polivkadev</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%2F459371%2F057547d7-650d-485c-b226-ead558fb95d3.jpg</url>
      <title>DEV Community: Pavel Polívka</title>
      <link>https://dev.to/polivkadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/polivkadev"/>
    <language>en</language>
    <item>
      <title>I got tired of Google Discover, so I built my own news feed in a day</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Wed, 11 Nov 2026 21:58:10 +0000</pubDate>
      <link>https://dev.to/polivkadev/i-got-tired-of-google-discover-so-i-built-my-own-news-feed-in-a-day-1g2c</link>
      <guid>https://dev.to/polivkadev/i-got-tired-of-google-discover-so-i-built-my-own-news-feed-in-a-day-1g2c</guid>
      <description>&lt;p&gt;I love the swipe left on my phone's home screen. Coffee in one hand, thumb swipes left, and I see what is going on in the world. It is one of the few phone habits I actually like.&lt;/p&gt;

&lt;p&gt;What I don't like is what I see there. So I built my own feed, called it Samizdat, and put it exactly where Google Discover used to be.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why Google Discover and Samsung News drove me crazy&lt;/li&gt;
&lt;li&gt;the small hack to get my own widget on the left page of a Samsung phone&lt;/li&gt;
&lt;li&gt;the Cloudflare setup behind it, and the one query that blew my free plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I have a Samsung Galaxy S24 Ultra. On One UI the page left of your home screen can show exactly three things: Google Discover, Samsung Free (now Samsung News), or nothing. That's it.&lt;/p&gt;

&lt;p&gt;I tried both. Both are bad in the same ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paywalls everywhere.&lt;/strong&gt; Every third story is a site I don't pay for. I tap, I get a "subscribe to continue" box. Why show it to me then?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Junk.&lt;/strong&gt; Celebrity gossip, sports I don't follow, "you won't believe what this actor did". I never clicked on any of it and it keeps coming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same story five times.&lt;/strong&gt; One press release, five sites, five cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No control.&lt;/strong&gt; You can tap "not interested" all day long. A week later it is back. There's no place to say "no sports, nothing older than two days, I like long articles more than breaking news".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I read Czech news, tech and dev stuff, and a bunch of hobby niches: RTS games, space, Linux on Apple Silicon, Home Assistant, Blender, electronics. Discover kind of knows that, but it mixes it with whatever pays the bills.&lt;/p&gt;

&lt;p&gt;So the idea was simple. I pick the sources, RSS like in the old days. An LLM reads every new article, throws out the junk and paywalls, merges duplicates and writes a short summary. My phone shows the top 30.&lt;/p&gt;

&lt;h2&gt;
  
  
  The left page hack
&lt;/h2&gt;

&lt;p&gt;The first question was where to show it. I didn't want another app I have to open. The whole point is the swipe.&lt;/p&gt;

&lt;p&gt;You can't put your own app on that left page on stock One UI. Custom launchers like Lawnchair, Neo Launcher or Nova with HomeFeeder can host a real feed page, but I didn't want to leave Samsung's launcher just for this.&lt;/p&gt;

&lt;p&gt;The trick is a widget:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Turn off the Discover page (long press the home screen, swipe left, switch it off).&lt;/li&gt;
&lt;li&gt;Add a new home screen page on the left of your main one.&lt;/li&gt;
&lt;li&gt;Put a widget on it and stretch it to full size.&lt;/li&gt;
&lt;li&gt;Keep your normal page set as home.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now when I unlock the phone I land on my usual home screen, swipe left, and there is my feed. Same gesture, my content. You can't really tell it is not a "real" feed page.&lt;/p&gt;

&lt;p&gt;The widget is built with Jetpack Glance, the Compose-style API for Android widgets. Every row is a small thumbnail, headline, source, age and "+3 sources" when a story got merged. The top story gets a bigger hero card. Tap opens the app on the AI summary first, then the clean full text, or the original if I really want it. Thumbs up, thumbs down, mute source, mute topic, read later.&lt;/p&gt;

&lt;p&gt;Glance has its limits (you can't just load an image URL, you have to download it and turn it into a bitmap yourself), but for a list of 30 items it is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure
&lt;/h2&gt;

&lt;p&gt;I wanted this to cost almost nothing and to need zero babysitting. Everything runs on Cloudflare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workers&lt;/strong&gt; with Hono. One Worker serves the API (&lt;code&gt;/api/*&lt;/code&gt;) and the settings web app as static assets. No separate Pages project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron Triggers.&lt;/strong&gt; At :00 and :30 it fetches about 70 feeds (with ETag and Last-Modified, so most fetches are cheap). At :15 and :45 it rebuilds the ranked feed. Once a night it cleans up anything older than 14 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queues.&lt;/strong&gt; Every new article goes to a queue and gets scored in batches of 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scoring itself&lt;/strong&gt; is one Claude Haiku call per article. It returns strict JSON: relevance against my rules, paywall verdict, topics, and a 2 to 3 sentence summary in the article's own language. Czech stays Czech. If Haiku is not available it falls back to Mistral Small on Workers AI and switches back on its own. (Fun fact: the fallback was the first thing that ran in production, because I forgot to top up my Anthropic credit.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers AI + Vectorize.&lt;/strong&gt; Every article gets a &lt;code&gt;bge-m3&lt;/code&gt; embedding (it is multilingual, which matters with Czech). That is how the same story from five sites becomes one item, and how "more like the stuff I thumbed up" works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D1&lt;/strong&gt; for sources, articles, scores and my signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Access&lt;/strong&gt; in front of everything. I log in with a one-time PIN to my email, the Android app uses a service token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ranking is nothing fancy: LLM score × freshness decay × my signals × source weight × similarity to what I liked. Rules are plain sentences like "no celebrity gossip" or "prefer in-depth over breaking", and they go straight into the prompt.&lt;/p&gt;

&lt;p&gt;The settings app is a small Preact SPA where I manage feeds, import OPML, switch on curated packs (Czech news, Czech tech, AI, games, space, Linux, home automation and so on) and write the rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query that ate my free plan
&lt;/h2&gt;

&lt;p&gt;One lesson I want to share, because it can happen to you too.&lt;/p&gt;

&lt;p&gt;To check if a feed item is new, I had this:&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="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;guid&lt;/span&gt; &lt;span class="o"&gt;=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks innocent. But an &lt;code&gt;OR&lt;/code&gt; across different columns can't use an index in SQLite, so every check scanned the whole table. And D1 bills every scanned row as a row read. Run that for every item in every feed every few minutes and you get 8.9 million rows read in a day. The free plan has 5 million, per account. So it also took down my other app on the same account until midnight UTC.&lt;/p&gt;

&lt;p&gt;The fix was an indexed lookup on the URL and skipping items older than the last fetch before touching the database at all. Two tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt; on your hot queries. &lt;code&gt;SCAN&lt;/code&gt; is bad, &lt;code&gt;SEARCH ... USING INDEX&lt;/code&gt; is what you want.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrangler d1 insights&lt;/code&gt; shows which query eats your reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added a daily budget in code. Every D1 result has &lt;code&gt;meta.rows_read&lt;/code&gt;, so the Worker sums it and stops background jobs when it gets too high. And I moved to Workers Paid for $5 a month, which is a fair price for not thinking about it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Honestly, most of the code was written by Claude Code. I did a brainstorm and a technical interview with it one evening, wrote down the decisions, and let it build the whole thing the next day: the Worker, the feed packs, the settings app, the Android app and the GitHub Actions pipeline that builds the APK (and now uploads to Google Play internal testing). My job was making decisions, unblocking things and reviewing.&lt;/p&gt;

&lt;p&gt;From idea to a widget on my phone took about a day. A few years ago this would be a month of weekends, and I would give up somewhere around the Android widget part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;The real test is simple: do I stop opening Google Discover? Ask me in a month.&lt;/p&gt;

&lt;p&gt;If you have a Samsung and you hate the left page too, the widget trick works with any feed reader widget, you don't need to build your own. But if you like building things, an LLM sitting between you and RSS is a really nice filter.&lt;/p&gt;

&lt;p&gt;How do you read news on your phone? Drop a comment, I'm curious.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>android</category>
      <category>ai</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I review my AI agent's code like a merge request</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:32:09 +0000</pubDate>
      <link>https://dev.to/polivkadev/i-review-my-ai-agents-code-like-a-merge-request-3elj</link>
      <guid>https://dev.to/polivkadev/i-review-my-ai-agents-code-like-a-merge-request-3elj</guid>
      <description>&lt;p&gt;These days most of the code in my side projects is written by Claude Code. I describe what I want, it writes, I review. My job moved from writing code to reviewing it.&lt;/p&gt;

&lt;p&gt;And reviewing in a terminal is bad. You get a wall of diff, scroll up, scroll down, and then type something like "in server.ts, around line 40, the host should be configurable" into the chat. It works, but it's nothing like a real code review. So I built a small tool for it. It's called Feedbacker, and it gives the agent a merge request page it can open for me.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what Feedbacker does&lt;/li&gt;
&lt;li&gt;how the agent asks for a review&lt;/li&gt;
&lt;li&gt;what I changed after the first real reviews&lt;/li&gt;
&lt;li&gt;the decisions and gotchas along the way&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;A few years ago I wrote about how to be good at code reviews. Most of it still holds when the author is an AI. What doesn't hold is the tooling. GitHub and GitLab are great for reviewing, but I don't want to push every half-done change to a branch and open an MR just to tell the agent it named something wrong.&lt;/p&gt;

&lt;p&gt;Two more things made it worse for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Several repos at once.&lt;/strong&gt; One task often touches more than one repo, and I want to see the whole change together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git worktrees.&lt;/strong&gt; I sometimes run more than one agent in parallel, each in its own worktree. Reviewing that in a terminal is no fun at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted: the agent finishes, opens a page in my browser with the diffs of everything it changed, I comment on lines like in an MR, click a button, and my comments go back to the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfd99dat4p3in6s4uqsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfd99dat4p3in6s4uqsy.png" alt="Feedbacker review page" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All repos on one page.&lt;/strong&gt; The agent passes a list of repo paths, anywhere on disk, each with its own base. With &lt;code&gt;--worktrees&lt;/code&gt; it pulls in every worktree of a repo, labelled with its branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line comments.&lt;/strong&gt; Click a line number to comment, shift-click to cover a range. A comment is either &lt;strong&gt;Comment&lt;/strong&gt; (change something) or &lt;strong&gt;Ask&lt;/strong&gt; (a question the agent has to answer).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve or Request changes.&lt;/strong&gt; That sends everything back to the agent and closes the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where to look.&lt;/strong&gt; The agent writes a short summary and points me to the places it thinks matter most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorted by kind.&lt;/strong&gt; Code first, then tests, config, docs and generated files (lock files, build output). Only code starts expanded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local only.&lt;/strong&gt; It runs on &lt;code&gt;127.0.0.1&lt;/code&gt; with a random token in the URL, only reads git, and nothing leaves my machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the agent asks
&lt;/h2&gt;

&lt;p&gt;The easiest way is the Claude Code plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add PavlikPolivka/feedbacker
claude plugin install feedbacker@feedbacker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I just say "let me review it". The plugin gives Claude an MCP tool, &lt;code&gt;request_review&lt;/code&gt;, and a skill that tells it when to use it. The tool call blocks until I submit, and the result is markdown the agent can act on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;# Review feedback
&lt;/span&gt;
Verdict: &lt;span class="gs"&gt;**changes requested**&lt;/span&gt;

&lt;span class="gu"&gt;## Comments
&lt;/span&gt;
&lt;span class="gu"&gt;### /home/me/Projects/api
&lt;/span&gt;
&lt;span class="gs"&gt;**src/server.ts:6**&lt;/span&gt; (new side)

&lt;span class="k"&gt;```diff
+ &lt;/span&gt; server.listen(port, "127.0.0.1");
&lt;span class="k"&gt;```
&lt;/span&gt;
Make the host configurable too.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every comment carries the code it refers to. If the lines move, the agent still knows what I meant.&lt;/p&gt;

&lt;p&gt;It also works without Claude Code. Any agent that speaks MCP can run it with &lt;code&gt;npx -y agent-feedbacker mcp&lt;/code&gt;, and anything with a shell can use the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;feedbacker ~/Projects/api ~/Projects/web@main -m &lt;span class="s2"&gt;"Bind the API to localhost"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It blocks, and when I submit it prints the feedback and exits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback on the feedback tool
&lt;/h2&gt;

&lt;p&gt;The first real review was Feedbacker reviewing itself. I had the agent rewrite the review page in React in a separate worktree, and then I reviewed that change in the old page. It worked, which was a nice moment.&lt;/p&gt;

&lt;p&gt;Then I used it for a few more changes and wrote down what annoyed me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The agent's questions felt random.&lt;/strong&gt; I had made it possible for the agent to ask me questions. In practice it asked things just to ask something. What I actually wanted was the other way around: to ask &lt;em&gt;it&lt;/em&gt; about a line. That's where &lt;strong&gt;Ask&lt;/strong&gt; came from. The agent can still ask, but only about a decision it can't make alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The summary was unreadable.&lt;/strong&gt; The agent wrote a long paragraph and the page showed it as one huge heading. Now it's short markdown: one lead sentence and a list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I don't want to review everything.&lt;/strong&gt; Lock files, &lt;code&gt;CLAUDE.md&lt;/code&gt;, build output. Now files are grouped by kind and the boring ones are collapsed. Tests are detected by path in most languages (&lt;code&gt;*.spec.ts&lt;/code&gt;, &lt;code&gt;*_test.go&lt;/code&gt;, &lt;code&gt;test_*.py&lt;/code&gt;, &lt;code&gt;FooTest.java&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell me where to look.&lt;/strong&gt; A good MR description points the reviewer at the tricky parts. The agent should do the same, so now it can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Funny thing. Most of this is the same advice I'd give a human developer opening an MR. Write a short description, point at what matters, don't make the reviewer read generated files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No runtime dependencies.&lt;/strong&gt; Plain Node with a hand-written minimal MCP server (it's just JSON-RPC over stdio) instead of the SDK. The page is a small React app, but the bundle is committed, so using it needs no install or build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own diff table.&lt;/strong&gt; I wanted to use diff2html, but it has no inline comments. The table is small and makes anchoring comments easy. Syntax highlighting is highlight.js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One blocking tool call.&lt;/strong&gt; I expected timeouts and planned two tools, one to start a review and one to wait for it. Claude Code kept the call open for over three minutes without complaining, so one tool it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The agent commits along the way.&lt;/strong&gt; Then "diff against HEAD" is empty and the review shows nothing. The plugin now uses hooks to record where each repo stood when the session first touched it, and the review shows exactly what changed in this session. Uncommitted work is snapshotted with &lt;code&gt;git stash create&lt;/code&gt;, which touches neither your working tree nor your stash list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't see the URL.&lt;/strong&gt; Claude Code doesn't show an MCP server's output, so if the browser doesn't open, the link is lost. &lt;code&gt;feedbacker list&lt;/code&gt; prints the reviews waiting for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The name was taken.&lt;/strong&gt; &lt;code&gt;feedbacker&lt;/code&gt; on npm belongs to someone else. The package is &lt;code&gt;agent-feedbacker&lt;/code&gt;, the command is still &lt;code&gt;feedbacker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI hung forever.&lt;/strong&gt; The end-to-end test runs the page in headless Chrome, and Chrome's sandbox doesn't start on GitHub's Ubuntu 24.04 runners. It didn't fail, it just hung.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other tools
&lt;/h2&gt;

&lt;p&gt;I'm not the first one with this idea. When I looked, there were already several: &lt;a href="https://github.com/backnotprop/plannotator" rel="noopener noreferrer"&gt;Plannotator&lt;/a&gt;, &lt;a href="https://github.com/yoshiko-pg/difit" rel="noopener noreferrer"&gt;difit&lt;/a&gt;, &lt;a href="https://github.com/rubymatrix/claude-diff-review" rel="noopener noreferrer"&gt;claude-diff-review&lt;/a&gt; and more. Plannotator is the closest. It can even review several repos, if they all sit under one parent folder.&lt;/p&gt;

&lt;p&gt;I looked at them, but they felt too complex for what I wanted. And I wanted any repos from anywhere and parallel worktrees side by side in one review. So I built my own small one. If one of those fits you better, use it. The idea matters more than the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth it?
&lt;/h2&gt;

&lt;p&gt;For me, yes. It's still early and the roadmap is long (replies to comments, showing only what changed since the last round), but reviewing in a browser is so much nicer than typing line numbers into a chat. And the agent gets better feedback, because I actually read the diff.&lt;/p&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/PavlikPolivka/feedbacker" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and the package is &lt;a href="https://www.npmjs.com/package/agent-feedbacker" rel="noopener noreferrer"&gt;&lt;code&gt;agent-feedbacker&lt;/code&gt;&lt;/a&gt; on npm.&lt;/p&gt;

&lt;p&gt;How do you review what your agent writes? Drop a comment, I'm curious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Google Sheets is my favourite free backend</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Tue, 03 Nov 2026 20:21:50 +0000</pubDate>
      <link>https://dev.to/polivkadev/google-sheets-is-my-favourite-free-backend-4no5</link>
      <guid>https://dev.to/polivkadev/google-sheets-is-my-favourite-free-backend-4no5</guid>
      <description>&lt;p&gt;I made a small game for my daughter. It's called Počítadlo ("counter" in Czech), and it's a simple counting game that helps her learn the basics of math: adding and subtracting up to 20, ten questions, a timer and a trophy when she beats her record. It runs as a PWA on her iPad.&lt;/p&gt;

&lt;p&gt;After a few days I wanted to know how she actually uses it. How often does she play, which levels, how long does it take her, where does she make mistakes? And I wanted it as an email to me and my wife every evening, because nobody is going to open a dashboard.&lt;/p&gt;

&lt;p&gt;The obvious answer is a database, an API and some email service. For a kids' game that's way too much. So I used a Google Sheet and Apps Script. It costs nothing, there's no server, and it took one evening, most of which was me fighting Google's settings.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the app sends data to a Google Sheet&lt;/li&gt;
&lt;li&gt;how the Sheet sends a daily email&lt;/li&gt;
&lt;li&gt;the gotchas that cost me most of that evening&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Every Google Sheet can have an Apps Script attached to it (Extensions → Apps Script). Apps Script is just JavaScript running on Google's servers, and it can do two things that matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be deployed as a &lt;strong&gt;web app&lt;/strong&gt;: you get a URL, and &lt;code&gt;doPost(e)&lt;/code&gt; runs every time someone POSTs to it.&lt;/li&gt;
&lt;li&gt;Run functions on a &lt;strong&gt;schedule&lt;/strong&gt; and send email with &lt;code&gt;MailApp&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the Sheet is the database, the script is the API, and Gmail is the email service. All free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receiving data
&lt;/h2&gt;

&lt;p&gt;The script side is tiny. Parse the JSON, append rows:&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;function&lt;/span&gt; &lt;span class="nf"&gt;doPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contents&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="n"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSpreadsheet&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="nf"&gt;getSheetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"events"&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="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&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;new&lt;/span&gt; &lt;span class="nb"&gt;Date&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="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mistakes&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="n"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLastRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ContentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTextOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ok"&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;p&gt;Then Deploy → New deployment → Web app, &lt;strong&gt;Execute as: Me&lt;/strong&gt;, &lt;strong&gt;Who has access: Anyone&lt;/strong&gt;. You get a URL ending with &lt;code&gt;/exec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the app, I send events like "app opened", "level started", "level finished in 53 seconds with one mistake". The call looks like this:&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;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"no-cors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text/plain"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&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;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things here are important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text/plain&lt;/code&gt;, not &lt;code&gt;application/json&lt;/code&gt;.&lt;/strong&gt; A JSON content type makes the browser send a CORS preflight, and Apps Script can't answer it. With &lt;code&gt;text/plain&lt;/code&gt; there's no preflight, and you still &lt;code&gt;JSON.parse&lt;/code&gt; it on the other side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;no-cors&lt;/code&gt; means you can't read the response.&lt;/strong&gt; A resolved fetch only tells you the request reached Google. Not that the script worked. Keep that in mind, it bit me later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The iPad is not always online, so events go to a queue in localStorage first and get sent when there's a connection. Nothing gets lost when she plays in the car.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending the email
&lt;/h2&gt;

&lt;p&gt;The report is a normal function. It reads the rows, counts games, best and average times per level, and lists the wrong answers. Then:&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="nc"&gt;MailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;EMAILS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Počítadlo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;htmlBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&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;p&gt;The &lt;code&gt;name&lt;/code&gt; matters more than you'd think. Without it the email comes "from you", Gmail files it under Sent, and you think it never arrived. Ask me how I know.&lt;/p&gt;

&lt;p&gt;For the schedule, you create a trigger:&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;function&lt;/span&gt; &lt;span class="nf"&gt;setup&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="nc"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeBased&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="nf"&gt;everyMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;p&gt;&lt;code&gt;check&lt;/code&gt; does nothing before 19:00. After that, it sends the report once and remembers the date in &lt;code&gt;PropertiesService&lt;/code&gt;. Why not just a daily trigger at 19:00? Read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;This is where my evening went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trigger doesn't exist until you run &lt;code&gt;setup&lt;/code&gt; by hand.&lt;/strong&gt; Obvious in hindsight. Pasting the code creates nothing. You have to pick &lt;code&gt;setup&lt;/code&gt; in the function dropdown and press Run. My first evening email never came because there was no trigger at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"At 19:00" means "sometime between 19:00 and 20:00".&lt;/strong&gt; Google runs time triggers at any minute inside the window. A 15-minute check that sends on its first run after 19:00 gets you within 15 minutes, and if one run fails, the next one catches up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;403 "access denied" with access set to Anyone.&lt;/strong&gt; The web app returned Google Drive's "you need access" page to everyone, even though the settings were right. It went away after I ran a function in the editor and approved the permissions. The script had simply never been authorized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Sorry, unable to open the file at this time."&lt;/strong&gt; That's Apps Script telling you that you're logged into more than one Google account. Use a private window with only the account that owns the Sheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New deployment vs. new version.&lt;/strong&gt; Deploy → New deployment gives you a &lt;strong&gt;new URL&lt;/strong&gt;. To update the code and keep the URL, go to Manage deployments → Edit → Version: New version. The scheduled functions always run the latest saved code, so changes to the email need only a save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it tell you it's alive.&lt;/strong&gt; Since &lt;code&gt;no-cors&lt;/code&gt; hides everything, I added a &lt;code&gt;doGet&lt;/code&gt; that lists the triggers and when the last report went out. Opening the URL in a browser shows me whether the schedule exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your hosting will play your game.&lt;/strong&gt; The same evening there were app opens with no games in them. It was Vercel, opening every new deploy in a headless browser to take a screenshot for the dashboard. Now everything not running as the installed app gets a &lt;code&gt;web-&lt;/code&gt; prefix on its session ID, and the email skips those. I don't drop them, just mark them. If the detection is ever wrong on the iPad, I still have the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;This is not a real backend and I wouldn't pretend it is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The web app URL is in your JavaScript, so anyone can POST to it. Validate what comes in. Worst case for me is junk rows in a spreadsheet.&lt;/li&gt;
&lt;li&gt;A free Gmail account can send 100 emails a day with &lt;code&gt;MailApp&lt;/code&gt;. For one report a day, fine.&lt;/li&gt;
&lt;li&gt;It's slow and has quotas. For a few hundred rows a day from one iPad, I'll never notice. For your startup, use a real database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Yes. For something like this, it's perfect. The whole thing is one script file, a spreadsheet I can open on my phone, and an email at 19:00. I already know she gets stuck on 20 − 7, and that she beat her Level 1 record in her second game of the evening.&lt;/p&gt;

&lt;p&gt;If you have a small personal project that needs to store a bit of data or send you a summary, try a Sheet before you spin up a server. The code is on &lt;a href="https://github.com/PavlikPolivka/pocitadlo" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; in the &lt;code&gt;tracking&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;What's your favourite "free backend" hack? Drop a comment.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My Obsidian vault, written by Claude Code, in my pocket</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:16:39 +0000</pubDate>
      <link>https://dev.to/polivkadev/my-obsidian-vault-written-by-claude-code-in-my-pocket-94l</link>
      <guid>https://dev.to/polivkadev/my-obsidian-vault-written-by-claude-code-in-my-pocket-94l</guid>
      <description>&lt;p&gt;These days I do most of my side-project work with Claude Code in a terminal. A few days ago I gave it access to my Obsidian vault and told it to write things down: every fix, every setup decision, every project. It turned out to be the best habit we built. The vault fills itself while I work.&lt;/p&gt;

&lt;p&gt;There was one problem. All of it lived on my laptop, and my best questions come when I'm away from it. In the car, on the couch, in a queue. So I put Claude Code on my home server next to a synced copy of the vault and now I talk to it from the Claude app on my phone.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the vault is organised so an AI agent can find things&lt;/li&gt;
&lt;li&gt;the setup on the server: Obsidian headless Sync and Claude Code Remote Control&lt;/li&gt;
&lt;li&gt;how I locked it down&lt;/li&gt;
&lt;li&gt;what broke on the way&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A vault that agents can read
&lt;/h2&gt;

&lt;p&gt;If you let an agent write into your notes without rules, you get a mess in a week. So the first note in my vault is &lt;code&gt;Vault Rules.md&lt;/code&gt;. It says where things go and what Claude may and may not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Folders:&lt;/strong&gt; &lt;code&gt;Inbox/&lt;/code&gt;, &lt;code&gt;Daily/&lt;/code&gt;, &lt;code&gt;Projects/&lt;/code&gt;, &lt;code&gt;Knowledge/&lt;/code&gt;, &lt;code&gt;Archive/&lt;/code&gt;, &lt;code&gt;Templates/&lt;/code&gt;, &lt;code&gt;Attachments/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontmatter on every note:&lt;/strong&gt; &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;. The &lt;code&gt;author&lt;/code&gt; field says if I wrote it, Claude did, or both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never delete or rewrite my text.&lt;/strong&gt; Claude adds below it or in a callout. To get rid of something, move it to &lt;code&gt;Archive/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log work in the daily note&lt;/strong&gt; under a &lt;code&gt;## Claude&lt;/code&gt; heading, one line per item, with links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No secrets.&lt;/strong&gt; Passwords live in Bitwarden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My global &lt;code&gt;CLAUDE.md&lt;/code&gt; points to this note, so every Claude Code session reads it before writing anything.&lt;/p&gt;

&lt;p&gt;Two more notes help agents find their way around without reading the whole vault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Me.md&lt;/code&gt; is a profile: what I work on, my devices, how I like answers. Claude wrote it from my notes and a short interview, and anything it guessed is marked &lt;em&gt;(inferred)&lt;/em&gt;. I correct it when it is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Vault Map.md&lt;/code&gt; lists every note in one line, explains my own terms and says how to search. When a note is added or moved, Claude updates the map in the same change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sounds like a lot of overhead. It isn't, because I don't do it. I just read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan
&lt;/h2&gt;

&lt;p&gt;I had three options for the phone part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build my own small chat web app on top of the Claude Agent SDK.&lt;/li&gt;
&lt;li&gt;Write a Telegram bot.&lt;/li&gt;
&lt;li&gt;Use Claude Code's &lt;strong&gt;Remote Control&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remote Control won. You run &lt;code&gt;claude remote-control&lt;/code&gt; in a folder on any machine, and the session shows up in the Claude app under Code. The server only makes outgoing connections, so there's nothing to expose to the internet, no reverse proxy and no login page to maintain. It runs on my normal Claude subscription.&lt;/p&gt;

&lt;p&gt;The vault also has to be on the server. I use Obsidian Sync between my devices, and Obsidian now has an official headless client for it. The server becomes just another synced device. When Claude edits a note there, I see it on my phone a few seconds later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obsidian headless Sync
&lt;/h2&gt;

&lt;p&gt;The client is the &lt;code&gt;obsidian-headless&lt;/code&gt; package on npm, and the command is &lt;code&gt;ob&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;npm install -g --prefix ~/.local obsidian-headless
ob login
ob sync-setup --vault Pavel --path ~/vault/Pavel --device-name home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to be careful about. Obsidian Sync syncs deletions too. An empty folder that pushes its "state" to your other devices is the nightmare scenario. So the first sync only downloads, and I switched it to two-way after I checked the file count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ob sync-config --path ~/vault/Pavel --mode pull-only
ob sync --path ~/vault/Pavel
find ~/vault/Pavel -type f -not -path &lt;span class="s1"&gt;'*/.obsidian/*'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; wc -l   &lt;span class="c"&gt;# same as on the laptop?&lt;/span&gt;
ob sync-config --path ~/vault/Pavel --mode bidirectional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, a systemd user service keeps it running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.config/systemd/user/obsidian-sync.service
&lt;/span&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pavel&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ob&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;continuous&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pavel&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;Pavel&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget &lt;code&gt;loginctl enable-linger&lt;/code&gt; for your user, or user services stop when you log out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code next to the vault
&lt;/h2&gt;

&lt;p&gt;I did not want Claude's config inside the vault, because everything in there syncs to all my devices. So Claude gets its own folder, &lt;code&gt;~/obsidian-agent&lt;/code&gt;, with two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;: read &lt;code&gt;Vault Rules.md&lt;/code&gt; first, then a few rules for this server. There's no Obsidian app here, so no Obsidian CLI; it edits the Markdown files directly. And keep answers short, because I read them on a phone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt;: what it is allowed to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The vault itself is added as an extra directory. Then another user service runs Remote Control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.config/systemd/user/claude-vault.service
&lt;/span&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pavel&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;obsidian&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;control&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nc"&gt;Vault&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="n"&gt;acceptEdits&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;spawn&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;acceptEdits&lt;/code&gt; means it can edit files in the vault without asking me each time. On a phone, approving every edit gets old fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking it down
&lt;/h2&gt;

&lt;p&gt;Remote Control is reachable from anywhere I'm logged in to my Claude account. That's the point, but it also means the session should not be able to do much more than I need. My rule: read and write the vault, search the web, nothing else. No shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"defaultMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"acceptEdits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"additionalDirectories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/home/pavel/vault/Pavel"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"Read(//home/pavel/vault/Pavel/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Edit(//home/pavel/vault/Pavel/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"WebSearch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Read(//home/pavel/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Read(//home/pavel/.config/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Read(//home/pavel/.claude/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Read(//etc/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Edit(//home/pavel/obsidian-agent/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Edit(//home/pavel/vault/Pavel/.obsidian/**)"&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;p&gt;Notice the double slash. That one bit us. In Claude Code permission rules, &lt;code&gt;/home/pavel/.ssh/**&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; mean an absolute path. A single leading slash is relative to the settings file. For an absolute path you need &lt;code&gt;//&lt;/code&gt;, or &lt;code&gt;~/&lt;/code&gt; for your home. With one slash, my deny rules matched nothing, and nothing warned me. We only caught it because we tested: we asked the agent to read &lt;code&gt;~/.ssh/config&lt;/code&gt; and the error said "you haven't granted it yet" instead of "denied". With the fix, it says denied.&lt;/p&gt;

&lt;p&gt;So test your sandbox. Ask the agent to run a command, read a secret file and edit its own rules. All three should fail.&lt;/p&gt;

&lt;p&gt;The last line of defense is a backup that doesn't depend on Sync. Every night at 03:30 a timer tars the vault and copies it to my NAS, and the last 14 copies are kept. The vault is under a megabyte, so it costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;It was not all smooth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ob: command not found&lt;/code&gt;.&lt;/strong&gt; I installed it into &lt;code&gt;~/.local/bin&lt;/code&gt;, and on the server that wasn't on my zsh &lt;code&gt;PATH&lt;/code&gt;. The services use the full path, so only my shell was affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login failed.&lt;/strong&gt; &lt;code&gt;ob login&lt;/code&gt; said "please double check your email and password". Check that you can log in on the Obsidian website with the same details, and try typing the password instead of pasting it into the hidden prompt over SSH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Remote Control is not yet enabled for your account."&lt;/strong&gt; The server had a Claude Code from half a year ago. I updated it with &lt;code&gt;sudo npm install -g&lt;/code&gt;, and &lt;code&gt;claude --version&lt;/code&gt; still said the old version. Turns out there were two installs: an old one in &lt;code&gt;/usr/local&lt;/code&gt; that came first on the &lt;code&gt;PATH&lt;/code&gt;, and the new one in &lt;code&gt;/usr&lt;/code&gt;. The service was running the old one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Enable Remote Control? (y/n)".&lt;/strong&gt; The new version asks this once, and a systemd service has no keyboard. Run &lt;code&gt;claude remote-control&lt;/code&gt; once by hand, answer &lt;code&gt;y&lt;/code&gt;, then start the service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I use it for
&lt;/h2&gt;

&lt;p&gt;Mostly small things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What did I decide about X?" It searches the project notes and the daily log and answers with links.&lt;/li&gt;
&lt;li&gt;"Put this in my inbox." An idea, a link, a bug I just noticed while playing my own game.&lt;/li&gt;
&lt;li&gt;"How did I fix Y on my laptop?" The answer is usually in a &lt;code&gt;Knowledge/&lt;/code&gt; note that Claude wrote while fixing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is the same agent and the same rules as on my laptop, so the notes look the same no matter where they were written. That's the part I like the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Obsidian CLI on the server.&lt;/strong&gt; The CLI talks to a running Obsidian app, and there is none. The agent uses grep and file search instead. For a vault my size that finds everything; it does miss backlinks through aliases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shell.&lt;/strong&gt; On purpose. If I want it to do something on the server, I do that from my laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Working with Claude Code
&lt;/h2&gt;

&lt;p&gt;Same as last time, I want to be honest about who did the work. Claude Code planned this, checked what was already on the server, wrote all the config and found most of the problems above. It also made the permission-path mistake, and it was Claude that caught it, by testing instead of trusting its own config.&lt;/p&gt;

&lt;p&gt;There were things it was not allowed to do. The auto mode stopped it from installing the services that keep running on my server, so I did that myself. The logins, with passwords and 2FA, were also mine. I think that is the right split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;It's new, so ask me again in a month. But one evening of setup, no custom code and nothing open to the internet, and my notes now answer me from my pocket. If you already let an agent write your notes, this is a small step.&lt;/p&gt;

&lt;p&gt;If you have your own setup for talking to your notes from your phone, I'd love to see it. Drop a comment.&lt;/p&gt;




&lt;p&gt;You can follow me here on &lt;a href="https://dev.to/pavel_polivka"&gt;dev.to&lt;/a&gt; for more.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>ai</category>
      <category>homelab</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Omarchy on an M1 MacBook Air: what works, what broke and how I fixed it</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Thu, 29 Oct 2026 20:48:55 +0000</pubDate>
      <link>https://dev.to/polivkadev/omarchy-on-an-m1-macbook-air-what-works-what-broke-and-how-i-fixed-it-1e4</link>
      <guid>https://dev.to/polivkadev/omarchy-on-an-m1-macbook-air-what-works-what-broke-and-how-i-fixed-it-1e4</guid>
      <description>&lt;p&gt;I had an M1 MacBook Air and an itch to try &lt;a href="https://omarchy.org" rel="noopener noreferrer"&gt;Omarchy&lt;/a&gt;, DHH's opinionated Arch Linux setup with Hyprland. Omarchy runs on Apple Silicon through &lt;a href="https://asahilinux.org" rel="noopener noreferrer"&gt;Asahi Linux&lt;/a&gt; and a community build for Macs. Installing it was the easy part. Making it my daily driver took two days.&lt;/p&gt;

&lt;p&gt;I did most of the setup in a terminal with Claude Code, and I kept notes of everything that broke. In this article, I want to go over the problems I hit and how I solved them, so you don't have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an update that left me with a blank desktop&lt;/li&gt;
&lt;li&gt;external monitors through a DisplayLink dock&lt;/li&gt;
&lt;li&gt;apps that don't ship for ARM Linux&lt;/li&gt;
&lt;li&gt;Netflix and DRM&lt;/li&gt;
&lt;li&gt;making a Mac keyboard behave&lt;/li&gt;
&lt;li&gt;a custom theme to finish it off&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The hardware is a MacBook Air M1. Asahi Linux gives you the kernel and drivers, and the community Mac build of Omarchy puts the whole Omarchy desktop on top. The GPU works and sleep works.&lt;/p&gt;

&lt;p&gt;One thing to know before you start copying configs from the internet: the Hyprland version Omarchy ships (0.56) uses Lua for configuration. Most tutorials you will find are for the old format. Even &lt;code&gt;hyprctl dispatch&lt;/code&gt; takes Lua now:&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="c"&gt;# old syntax, fails&lt;/span&gt;
hyprctl dispatch workspace 8

&lt;span class="c"&gt;# new syntax&lt;/span&gt;
hyprctl dispatch &lt;span class="s2"&gt;"hl.dsp.focus({ workspace = '8' })"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always look at the output. The old syntax just prints an error, and if you pipe it to &lt;code&gt;/dev/null&lt;/code&gt; you will wonder why nothing happens.&lt;/p&gt;

&lt;p&gt;The first hour was the easy stuff. Zen as the default browser on Super + B, the browser always opening on workspace 1, the terminal on workspace 2, smaller gaps between windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ~/.config/hypr/hyprland.lua&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"^(zen)$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"^(foot|Alacritty|kitty|com.mitchellh.ghostty)$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2"&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;
  
  
  The update that broke my desktop
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time, and it was my own fault.&lt;/p&gt;

&lt;p&gt;I installed KDE Connect after refreshing the package database, but without a full system upgrade. On Arch that is a partial upgrade, and you should never do it. KDE Connect pulled in a newer Qt. I fixed it with a full &lt;code&gt;omarchy update&lt;/code&gt;, rebooted, pressed Super + Space and got an empty screen. The menu, the launcher and parts of the top bar were all blank.&lt;/p&gt;

&lt;p&gt;The first guess was a mismatch between Qt and the shell binary, so we rebuilt the shell from the AUR. It did not help. The real cause was more interesting: Qt 6.12 added a built-in type called &lt;code&gt;Color&lt;/code&gt;, and Omarchy's shell has its own &lt;code&gt;Color&lt;/code&gt; object. The new built-in one won, and every colour in the shell came back empty. Upstream Omarchy had already fixed it by importing the shell's version under a name (&lt;code&gt;Commons.Color&lt;/code&gt;). The Mac build had not shipped that fix yet.&lt;/p&gt;

&lt;p&gt;The fix: a patched copy of the shell, and pointing Omarchy at it. Omarchy reads its install path from &lt;code&gt;/etc/omarchy.conf&lt;/code&gt;, so the copy can live in your home folder:&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="c"&gt;# /etc/omarchy.conf&lt;/span&gt;
&lt;span class="nv"&gt;OMARCHY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/pavel/.local/share/omarchy-patched
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The copy symlinks everything from &lt;code&gt;/usr/share/omarchy&lt;/code&gt; except the &lt;code&gt;shell/&lt;/code&gt; folder, which has the upstream fix applied. Once the Mac build catches up, I delete the file and reboot. I also added a small post-update hook that warns me when Qt is newer than the shell, so I see it before the desktop does.&lt;/p&gt;

&lt;p&gt;Lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never &lt;code&gt;pacman -Sy&lt;/code&gt; without &lt;code&gt;-u&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When everything breaks after an update, look at which libraries changed before rebuilding things.&lt;/li&gt;
&lt;li&gt;Community plugins can have the same bug. I had to patch two of them the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  External monitors
&lt;/h2&gt;

&lt;p&gt;My desk has a Dell monitor in portrait and a Samsung 4K. On this MacBook I could not get video out through the USB-C ports, so I used a DisplayLink dock. On Arch that needs the &lt;code&gt;evdi-dkms&lt;/code&gt; and &lt;code&gt;displaylink&lt;/code&gt; packages from the AUR. After a reboot, both monitors came up.&lt;/p&gt;

&lt;p&gt;Getting them arranged had three small traps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Dell needs a rotation.&lt;/strong&gt; &lt;code&gt;transform = 1&lt;/code&gt; in the monitor config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The screens must touch.&lt;/strong&gt; If there is even a small gap between them in the layout, the mouse cannot move from one screen to the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nwg-displays is handy, but its Save overwrites your whole &lt;code&gt;monitors.lua&lt;/code&gt;.&lt;/strong&gt; Use it to drag screens around, then copy the positions into your own file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is what I ended up with. Matching monitors by description instead of connector name keeps it stable, because DisplayLink connector names can change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ~/.config/hypr/monitors.lua&lt;/span&gt;
&lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="n"&gt;dell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"desc:Dell Inc. DELL P2419HC 9N7BL03"&lt;/span&gt;
&lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="n"&gt;samsung&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"desc:Samsung Electric Company SAMSUNG"&lt;/span&gt;

&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1920x1080@60"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"424x2448"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&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="n"&gt;transform&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;samsung&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3840x2160@60"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1504x2448"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eDP-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2560x1600@60"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3904x3209"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Workspaces 1-7 on the Samsung, 8 on the Dell, 9-10 on the laptop&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ws&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="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workspace_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;samsung&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workspace_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;Without the dock, all workspaces simply fall back to the laptop screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  ARM apps: the long tail
&lt;/h2&gt;

&lt;p&gt;Most things are in the Arch Linux ARM repos. The ones that are not were the most work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What worked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bitwarden&lt;/td&gt;
&lt;td&gt;The bar plugin ships helper binaries only for Intel&lt;/td&gt;
&lt;td&gt;Built the three helpers with Rust (&lt;code&gt;cargo build --locked&lt;/code&gt;) and allowed ARM in the plugin's architecture check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;The web app works, but I wanted the native OmaWhatsApp plugin&lt;/td&gt;
&lt;td&gt;Built its privacy-patched &lt;code&gt;wacli&lt;/code&gt; from source with Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zed&lt;/td&gt;
&lt;td&gt;Not in the ARM repos&lt;/td&gt;
&lt;td&gt;The official installer has ARM builds. Added a &lt;code&gt;zeditor&lt;/code&gt; alias so Omarchy's menu sees it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blender&lt;/td&gt;
&lt;td&gt;No ARM Linux build from Blender, Flathub or Arch&lt;/td&gt;
&lt;td&gt;Fedora builds it for ARM, so it runs in a Fedora container with distrobox, GPU included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obsidian&lt;/td&gt;
&lt;td&gt;In the repos&lt;/td&gt;
&lt;td&gt;Turned on its command line tool, so Claude Code can read and write my notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few notes from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zed&lt;/strong&gt; is my IntelliJ replacement. It has a JetBrains keymap, and one setting turns off all AI features: &lt;code&gt;"disable_ai": true&lt;/code&gt;. I also hid the sign-in button, because I don't want an account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blender&lt;/strong&gt; from Fedora had one bug: its colour config is newer than its colour library, so it silently fell back to basic colours. Changing the version line in &lt;code&gt;config.ocio&lt;/code&gt; from 2.5 to 2.4 fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check what an installer does before you run it.&lt;/strong&gt; Some, like Zed's, just unpack into &lt;code&gt;~/.local&lt;/code&gt;. Some want root. Reading 170 lines of shell script takes two minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Netflix
&lt;/h2&gt;

&lt;p&gt;Netflix needs Google's Widevine DRM. Google makes ARM Linux Widevine only for Chromebooks, so Netflix only accepts it from a browser that says it is a Chromebook. I went through three error codes before it worked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F7701-1003 in Zen&lt;/td&gt;
&lt;td&gt;Widevine installed but not enabled in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E100&lt;/td&gt;
&lt;td&gt;Widevine works, but the browser doesn't say it is ChromeOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C7701-1003 in Chromium&lt;/td&gt;
&lt;td&gt;Fresh profile, Widevine downloaded but not loaded until restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What works is a separate Netflix web app in Chromium with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its own profile (&lt;code&gt;--user-data-dir&lt;/code&gt;), so the flags apply even when another Chromium window is already running&lt;/li&gt;
&lt;li&gt;a ChromeOS user agent&lt;/li&gt;
&lt;li&gt;the ChromeOS-based Widevine from Asahi's &lt;code&gt;widevine&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--disable-component-update&lt;/code&gt;, so Chromium doesn't replace that Widevine with its own download&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quality is capped at 720p, like on any Linux browser. Good enough for the couch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Mac keyboard behave
&lt;/h2&gt;

&lt;p&gt;After years on macOS, my fingers expect Cmd shortcuts. Omarchy already maps Super + C/V/X to copy and paste. I added Super + A for select all. It sends Ctrl + A, except in terminals, where Ctrl + A means "go to the start of the line".&lt;/p&gt;

&lt;p&gt;The F-row needed the most changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Media keys by default.&lt;/strong&gt; The Apple keyboard driver has a setting for this: &lt;code&gt;options hid_apple fnmode=1&lt;/code&gt; in &lt;code&gt;/etc/modprobe.d/hid_apple.conf&lt;/code&gt;. Brightness, media and volume work without Fn; F1–F12 need Fn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F5 is dictation.&lt;/strong&gt; On the Mac it is the dictation key; here it sends "mic mute". Omarchy ships Voxtype, a local speech-to-text tool, so F5 now does push-to-talk dictation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F4 opens the app launcher&lt;/strong&gt;, like Spotlight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F3 opens the screenshot menu.&lt;/strong&gt; Omarchy has no Mission Control, and Super + Space already opens the menu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F6 toggles Do Not Disturb.&lt;/strong&gt; By default it put the laptop to sleep, because systemd handles that key before Hyprland sees it. One line fixes that: &lt;code&gt;HandleSuspendKey=ignore&lt;/code&gt; in a drop-in under &lt;code&gt;/etc/systemd/logind.conf.d/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smaller things: I added the Czech QWERTY layout (Left Alt + Right Alt switches, and Omarchy's bar shows a layout indicator as soon as there are two), and I set the calendar to start weeks on Monday.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work
&lt;/h2&gt;

&lt;p&gt;To be fair, not everything is solved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Touch ID.&lt;/strong&gt; The fingerprint sensor is tied to Apple's Secure Enclave, and Asahi doesn't support it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic keyboard backlight.&lt;/strong&gt; There is no ambient light sensor support, so the backlight only turns off when the screen locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netflix in my normal browser.&lt;/strong&gt; Only the Chromebook-pretending web app plays it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some defaults assume a bigger screen.&lt;/strong&gt; The screenshot editor opened larger than the laptop display, with its toolbar off-screen. A window rule capping it at 85% of the screen fixed that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A theme to finish it off
&lt;/h2&gt;

&lt;p&gt;Once everything worked, I made it look the way I wanted. &lt;strong&gt;Golden Record&lt;/strong&gt; is my own Omarchy theme: deep-space blue-black, warm text, gold accents, and NASA and Webb images as backgrounds. The background follows the time of day: Earthrise in the morning, the Sun during the day, the golden Cosmic Cliffs in the evening, a deep field full of galaxies at night.&lt;/p&gt;

&lt;p&gt;You can install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omarchy theme install https://github.com/PavlikPolivka/golden-record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omarchy themes the terminal, bar, editor and Chromium from one &lt;code&gt;colors.toml&lt;/code&gt;, but not Firefox-based browsers like Zen. So I wrote that part too, and sent it upstream as a pull request to Omarchy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Claude Code
&lt;/h2&gt;

&lt;p&gt;I want to be honest about how this went, because I didn't type most of these commands myself.&lt;/p&gt;

&lt;p&gt;Claude Code did the research, read the source when something broke and wrote the configs. It was fast and it was thorough. It also made mistakes: it blamed the blank desktop on the wrong thing at first, it once killed its own shell with a too-wide &lt;code&gt;pkill -f&lt;/code&gt;, and it claimed a tool wouldn't touch my config when it did. Each time, the fix was the same: check the actual output instead of assuming.&lt;/p&gt;

&lt;p&gt;Some things it was not allowed to do: build AUR packages, install services that keep running, publish anything under my account. I ran those myself, and I think that is the right split.&lt;/p&gt;

&lt;p&gt;The best habit we built was writing everything down. Every fix went into my Obsidian notes as it happened. That is also why this article exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Yes. Two days in, the M1 Air runs Omarchy as my daily machine: three monitors, a keyboard that feels like a Mac, the apps I need and a desktop I like looking at. Expect to fix things yourself, especially anything that doesn't ship for ARM. If you're fine with that, it's a great second life for an M1.&lt;/p&gt;

&lt;p&gt;If you run Omarchy or Asahi on a Mac, I'd love to hear what you had to fix. Drop a comment.&lt;/p&gt;




&lt;p&gt;You can follow me here on &lt;a href="https://dev.to/pavel_polivka"&gt;dev.to&lt;/a&gt; for more.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>hyprland</category>
      <category>macbook</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Industry Quietly Killed Junior Developer Jobs</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Fri, 24 Apr 2026 19:47:12 +0000</pubDate>
      <link>https://dev.to/polivkadev/the-industry-quietly-killed-junior-developer-jobs-5eil</link>
      <guid>https://dev.to/polivkadev/the-industry-quietly-killed-junior-developer-jobs-5eil</guid>
      <description>&lt;h3&gt;
  
  
  The Path That Used to Exist
&lt;/h3&gt;

&lt;p&gt;A few years ago, the path into tech felt almost predictable. You learned how to code, landed a junior position, and slowly grew into a senior developer. It wasn’t easy, but it was clear.&lt;/p&gt;

&lt;p&gt;That path still exists on paper. In practice, it’s quietly breaking down.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed Inside Teams
&lt;/h3&gt;

&lt;p&gt;I lead a development team in a high-pressure environment, and over the past couple of years, something shifted. We didn’t make a big announcement. There was no strategic decision titled “Stop Hiring Juniors.” But when you look at what’s actually happening, the result is the same: we’re hiring fewer junior developers.&lt;/p&gt;

&lt;p&gt;Not because we don’t want to help people grow. But because the system we operate in stopped supporting it.&lt;/p&gt;

&lt;p&gt;At one point, we even had a discussion about shortening onboarding and training for new joiners. Not to improve the experience or make it more efficient in a meaningful way, but simply to spend less time on it. That alone says a lot. Training used to be an expected part of building a team. Now it’s something we actively try to minimize.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Didn’t Replace Developers — It Reshaped the Work
&lt;/h3&gt;

&lt;p&gt;Part of the shift comes from tools. AI didn’t replace developers, but it changed the distribution of work inside teams. Tasks that used to be perfect for juniors—writing boilerplate, digging through bugs, documenting messy legacy systems—are now handled by more experienced developers using AI assistance.&lt;/p&gt;

&lt;p&gt;The work didn’t disappear, but it became faster, more predictable, and easier to absorb without adding another person to the team.&lt;/p&gt;

&lt;p&gt;That changes the hiring equation in a very real way. When a senior developer can pick up a task and complete it quickly with the help of AI, the need to bring in someone junior to learn on that task becomes harder to justify.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Cost of Juniors
&lt;/h3&gt;

&lt;p&gt;The harder truth, though, has less to do with tools and more to do with time.&lt;/p&gt;

&lt;p&gt;Juniors don’t just cost salary. They cost attention. Explaining a task properly takes time. Providing context takes time. Reviewing code carefully takes time. And all of that happens while the rest of the team is under constant pressure to deliver.&lt;/p&gt;

&lt;p&gt;In theory, mentoring is an investment. In practice, it often feels like a delay.&lt;/p&gt;

&lt;p&gt;There’s a moment most lead developers recognize. You’re halfway through explaining a task, juggling deadlines, and thinking to yourself that you could probably just do it faster on your own—or now, with the help of AI tools.&lt;/p&gt;

&lt;p&gt;That moment doesn’t make you a bad mentor. It reflects the environment you’re operating in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Mentorship Is Breaking Down
&lt;/h3&gt;

&lt;p&gt;Mentoring requires space to slow down, and most teams don’t have that space anymore. Deadlines are tighter, expectations are higher, and teams are expected to deliver more with fewer people.&lt;/p&gt;

&lt;p&gt;In that kind of environment, juniors aren’t seen as future assets—they’re seen as immediate risk.&lt;/p&gt;

&lt;p&gt;Not because they lack potential, but because the system doesn’t allow them to be unproductive for long enough to grow.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pipeline Problem Nobody Solves
&lt;/h3&gt;

&lt;p&gt;This creates a deeper problem that the industry isn’t really addressing. Everyone wants senior developers, but senior developers don’t just appear. They come from junior roles, from time spent learning on the job, from being allowed to make mistakes and gradually improve.&lt;/p&gt;

&lt;p&gt;If we reduce those opportunities, we shrink the future pool of experienced developers.&lt;/p&gt;

&lt;p&gt;You can already see early signs of this shift. Job postings labeled as “junior” often require years of experience. True entry-level roles are harder to find. More companies expect candidates to arrive already capable of contributing almost immediately.&lt;/p&gt;

&lt;p&gt;The expectations didn’t just increase. The entry point moved.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Replaces Junior Roles
&lt;/h3&gt;

&lt;p&gt;What replaces the old junior path isn’t a single alternative, but a mix of changes. People spend longer learning on their own before landing their first job. Personal projects and real-world experience become essential just to be considered. AI tools act as a kind of substitute for entry-level work inside teams. And the number of opportunities to learn while being paid decreases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Means for New Developers
&lt;/h3&gt;

&lt;p&gt;For someone trying to break into the industry, that changes the advice we should be giving.&lt;/p&gt;

&lt;p&gt;“Learn to code and get a job” no longer reflects reality. A more honest version would focus on becoming useful before being hired—learning how to navigate real codebases, building things that actually work, and developing the ability to solve problems that aren’t clearly defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing
&lt;/h3&gt;

&lt;p&gt;None of this means junior developers are gone. But the version of the role that many people still imagine—the one where you join a team, get mentored, and gradually grow into the job—is fading.&lt;/p&gt;

&lt;p&gt;The industry didn’t make a big decision to remove it. It just optimized for speed, efficiency, and output.&lt;/p&gt;

&lt;p&gt;And quietly, that was enough.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Multi-Agent Content Management System with AI</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Mon, 13 Apr 2026 21:03:59 +0000</pubDate>
      <link>https://dev.to/polivkadev/building-a-multi-agent-content-management-system-with-ai-29i7</link>
      <guid>https://dev.to/polivkadev/building-a-multi-agent-content-management-system-with-ai-29i7</guid>
      <description>&lt;p&gt;I recently built something that would have sounded like science fiction a year ago: an AI system that helps manage parts of my content workflow—from discovery to publishing—across LinkedIn, BlueSky, and DEV.to.&lt;/p&gt;

&lt;p&gt;Not a chatbot. Not a simple automation script. A &lt;strong&gt;multi-agent orchestration system&lt;/strong&gt; where different AI agents coordinate to handle different parts of the content pipeline.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Context Switching is Killing Productivity
&lt;/h2&gt;

&lt;p&gt;Before this system, my content workflow looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manually browse Hacker News, Reddit, and tech blogs for topics&lt;/li&gt;
&lt;li&gt;Take notes in a doc somewhere&lt;/li&gt;
&lt;li&gt;Write drafts when I find time&lt;/li&gt;
&lt;li&gt;Edit and format for each platform separately&lt;/li&gt;
&lt;li&gt;Remember to actually post them&lt;/li&gt;
&lt;li&gt;Track what performed well&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step required context switching. Each context switch killed momentum. And worst of all, the coordination overhead meant I was spending more time &lt;em&gt;managing&lt;/em&gt; content than &lt;em&gt;creating&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Agents That Actually Coordinate
&lt;/h2&gt;

&lt;p&gt;Instead of building one monolithic AI assistant, I built a team of specialized agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery Agent&lt;/strong&gt;: Scans web sources, identifies trending topics relevant to my audience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Writer&lt;/strong&gt;: Takes approved topics and drafts initial content (which I then refine and personalize)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Manager&lt;/strong&gt;: Orchestrates publishing schedules, handles cross-platform posting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand Strategist&lt;/strong&gt;: Reviews content for brand voice and decides what's worth publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each agent has a specific job. Each agent can create tasks, update status, escalate when blocked, and hand off work to other agents.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;agents don't just execute tasks—they coordinate around tasks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important note&lt;/strong&gt;: The AI agents handle the boring coordination work—scheduling, formatting, API calls. The creative work, strategic decisions, and final writing? That's still me. Think of this as automation for the parts of content creation that don't require human creativity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Tasks as the Coordination Primitive
&lt;/h2&gt;

&lt;p&gt;The core of the system is a task queue, but not like you're thinking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│           Task Management Layer                 │
│  (Issues, Status, Assignments, Dependencies)    │
└─────────────────────────────────────────────────┘
         ↑              ↑              ↑
         │              │              │
    ┌────┴───┐     ┌───┴────┐    ┌───┴─────┐
    │Discovery│     │Content │    │Content  │
    │ Agent   │────&amp;gt;│Writer  │───&amp;gt;│Manager  │
    └─────────┘     └────────┘    └─────────┘
         │              │              │
         ↓              ↓              ↓
    ┌─────────────────────────────────────────┐
    │       External APIs (Web, LLM, Social)  │
    └─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every piece of work is an &lt;strong&gt;issue&lt;/strong&gt; (think GitHub issue, JIRA ticket, Linear task). Issues have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status&lt;/strong&gt;: &lt;code&gt;todo&lt;/code&gt;, &lt;code&gt;in_progress&lt;/code&gt;, &lt;code&gt;in_review&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assignee&lt;/strong&gt;: which agent owns this work right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parent/child relationships&lt;/strong&gt;: for breaking down complex work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies&lt;/strong&gt;: explicit "blocked by" relationships between tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an agent picks up work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It &lt;strong&gt;checks out&lt;/strong&gt; the issue (atomic lock, prevents conflicts)&lt;/li&gt;
&lt;li&gt;Does the work&lt;/li&gt;
&lt;li&gt;Updates status and adds a comment explaining what changed&lt;/li&gt;
&lt;li&gt;Either marks it &lt;code&gt;done&lt;/code&gt; or reassigns to another agent&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't novel in human project management. What's novel is that AI agents can now participate in the same workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Code: Publishing to BlueSky
&lt;/h2&gt;

&lt;p&gt;Here's what the Content Manager agent actually does when asked to publish a post:&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="c"&gt;# 1. Authenticate with BlueSky&lt;/span&gt;
&lt;span class="nv"&gt;AUTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;curl -s -X POST &lt;span class="s2"&gt;"https://bsky.social/xrpc/com.atproto.server.createSession"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s2"&gt;"{\"identifier\": \"$BSKY_IDENTIFIER\", \"password\": \"$BSKY_APP_PASSWORD\"}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;ACCESS_JWT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"$AUTH"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; python3 -c &lt;span class="s2"&gt;"import sys,json; print(json.load(sys.stdin)['accessJwt'])"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"$AUTH"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; python3 -c &lt;span class="s2"&gt;"import sys,json; print(json.load(sys.stdin)['did'])"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Create the post&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;date -u +&lt;span class="s2"&gt;"%Y-%m-%dT%H:%M:%S.000Z"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

curl -s -X POST &lt;span class="s2"&gt;"https://bsky.social/xrpc/com.atproto.repo.createRecord"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer $ACCESS_JWT"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s2"&gt;"{
    \"repo\": \"$DID\",
    \"collection\": \"app.bsky.feed.post\",
    \"record\": {
      \"\\$type\": \"app.bsky.feed.post\",
      \"text\": \"$POST_TEXT\",
      \"createdAt\": \"$TIMESTAMP\"
    }
  }"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy. Standard REST API calls. The magic isn't in the API integration—it's in the orchestration layer that knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When to post&lt;/li&gt;
&lt;li&gt;What content to use&lt;/li&gt;
&lt;li&gt;How to handle failures&lt;/li&gt;
&lt;li&gt;Who to notify when done&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Code: The Heartbeat Pattern
&lt;/h2&gt;

&lt;p&gt;Agents don't run continuously. They run in &lt;strong&gt;heartbeats&lt;/strong&gt;—short execution windows triggered by events.&lt;/p&gt;

&lt;p&gt;Each heartbeat follows this pattern:&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="c"&gt;# 1. Check what's assigned to me&lt;/span&gt;
&lt;span class="nv"&gt;INBOX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;curl -s -H &lt;span class="s2"&gt;"Authorization: Bearer $API_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"$API_URL/api/agents/me/inbox-lite"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Pick highest priority work (in_progress → todo → blocked if I can unblock)&lt;/span&gt;
&lt;span class="nv"&gt;ISSUE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"$INBOX"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; jq -r &lt;span class="s1"&gt;'.[0].id'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Checkout (atomic lock)&lt;/span&gt;
curl -s -X POST -H &lt;span class="s2"&gt;"Authorization: Bearer $API_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"X-Run-Id: $RUN_ID"&lt;/span&gt; \
  &lt;span class="s2"&gt;"$API_URL/api/issues/$ISSUE_ID/checkout"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Do the work&lt;/span&gt;
&lt;span class="c"&gt;# ... agent-specific logic here ...&lt;/span&gt;

&lt;span class="c"&gt;# 5. Update status&lt;/span&gt;
curl -s -X PATCH -H &lt;span class="s2"&gt;"Authorization: Bearer $API_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"X-Run-Id: $RUN_ID"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"status": "done", "comment": "Published to BlueSky: https://..."}'&lt;/span&gt; \
  &lt;span class="s2"&gt;"$API_URL/api/issues/$ISSUE_ID"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This heartbeat pattern means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents don't fight over the same work (checkout is atomic)&lt;/li&gt;
&lt;li&gt;Every action is traced (run ID links actions to heartbeat executions)&lt;/li&gt;
&lt;li&gt;Failures are isolated (one agent crashing doesn't affect others)&lt;/li&gt;
&lt;li&gt;The system is naturally resumable (agents pick up where they left off)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Hard Parts Nobody Talks About
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Error Recovery
&lt;/h3&gt;

&lt;p&gt;LLMs fail. APIs rate-limit. Networks drop. Your agents &lt;strong&gt;will&lt;/strong&gt; encounter errors.&lt;/p&gt;

&lt;p&gt;The first version of my system would just retry infinitely or silently fail. Neither is acceptable.&lt;/p&gt;

&lt;p&gt;Now, agents follow this escalation pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transient errors (network, rate limit): retry with exponential backoff&lt;/li&gt;
&lt;li&gt;Validation errors (bad input): mark issue &lt;code&gt;blocked&lt;/code&gt;, comment with specific error, reassign to creator&lt;/li&gt;
&lt;li&gt;Permanent errors (auth expired): mark &lt;code&gt;blocked&lt;/code&gt;, escalate to me
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$HTTP_STATUS&lt;/span&gt; -eq 401 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  curl -X PATCH -d &lt;span class="s1"&gt;'{
    "status": "blocked",
    "comment": "Authentication expired. Please refresh access token"
  }'&lt;/span&gt; &lt;span class="s2"&gt;"$API_URL/issues/$ISSUE_ID"&lt;/span&gt;
  &lt;span class="nb"&gt;exit&lt;/span&gt; 0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Content Length Limits
&lt;/h3&gt;

&lt;p&gt;BlueSky has a 300 &lt;strong&gt;grapheme&lt;/strong&gt; limit (not characters, not bytes—graphemes). An emoji counts as one grapheme, but &lt;code&gt;é&lt;/code&gt; might count as one or two depending on normalization.&lt;/p&gt;

&lt;p&gt;I learned this the hard way when a post with 280 characters got rejected for being 340 graphemes.&lt;/p&gt;

&lt;p&gt;Solution: validate before posting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your post text"&lt;/span&gt;
&lt;span class="n"&gt;grapheme_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Simplified; use grapheme library for production&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;grapheme_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Truncate or split into thread&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Daily Routines That Don't Spam
&lt;/h3&gt;

&lt;p&gt;I wanted daily publishing reminders, but I didn't want 7 separate tasks clogging my inbox.&lt;/p&gt;

&lt;p&gt;Solution: &lt;strong&gt;routines with concurrency policies&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Daily content reminder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"cronExpression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"30 8 * * *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"concurrencyPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"skip_if_active"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;skip_if_active&lt;/code&gt; means: if yesterday's reminder is still open, don't create today's. This prevents reminder spam while ensuring I don't lose track.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Enables
&lt;/h2&gt;

&lt;p&gt;With this system running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery Agent&lt;/strong&gt; scans Hacker News daily, creates issues for trending topics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand Strategist&lt;/strong&gt; reviews those issues, approves the ones worth writing about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Writer&lt;/strong&gt; drafts initial content based on approved topics (I refine and add my personal insights)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Manager&lt;/strong&gt; schedules and publishes across BlueSky, LinkedIn, DEV.to&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My involvement: strategic decisions, creative refinement, and final approval. The boring coordination work runs autonomously.&lt;/p&gt;

&lt;p&gt;Time saved per week: ~10 hours of coordination overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Agents Are APIs for Work
&lt;/h3&gt;

&lt;p&gt;Just like you wouldn't build an API that accepts ambiguous parameters, don't give agents ambiguous tasks.&lt;/p&gt;

&lt;p&gt;Bad: "Handle the deployment"&lt;br&gt;
Good: "Deploy branch &lt;code&gt;release/v2.3&lt;/code&gt; to production, run smoke tests, rollback if health checks fail after 5 min"&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Visibility &amp;gt; Autonomy
&lt;/h3&gt;

&lt;p&gt;Early on, I optimized for "how much can agents do without me?"&lt;/p&gt;

&lt;p&gt;Wrong question.&lt;/p&gt;

&lt;p&gt;Better question: "how quickly can I understand what agents did and why?"&lt;/p&gt;

&lt;p&gt;Now every agent action includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed&lt;/li&gt;
&lt;li&gt;Why it changed&lt;/li&gt;
&lt;li&gt;What happens next&lt;/li&gt;
&lt;li&gt;Links to relevant resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Start With Boring Tech
&lt;/h3&gt;

&lt;p&gt;The orchestration layer is PostgreSQL + REST APIs. No vector DB, no RAG, no fancy AI frameworks.&lt;/p&gt;

&lt;p&gt;Why? Because the hard part isn't the AI—it's the coordination. And coordination is a solved problem in distributed systems.&lt;/p&gt;

&lt;p&gt;Use LLMs for what they're good at (writing, reasoning, API calls). Use boring databases for what they're good at (consistency, transactions, queries).&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;You don't need to build the whole system at once. Start small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one recurring task (e.g., posting to social media)&lt;/li&gt;
&lt;li&gt;Create a simple task queue (even a JSON file works)&lt;/li&gt;
&lt;li&gt;Build one agent that reads from the queue, does the work, updates status&lt;/li&gt;
&lt;li&gt;Add a second agent that creates tasks for the first agent&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you see two agents coordinating, you'll understand why this pattern scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Approval workflows&lt;/strong&gt;: some tasks need human review before publishing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget controls&lt;/strong&gt;: LLM costs add up fast; agents need spending limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics integration&lt;/strong&gt;: track what content performs, feed that back to the Discovery Agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to remove humans from the loop—it's to remove humans from the &lt;em&gt;boring&lt;/em&gt; parts of the loop. The creative thinking, strategic decisions, and personal touch? That's still all human.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions?&lt;/strong&gt; Drop them in the comments. I'm especially curious if you've built similar multi-agent systems and what patterns worked (or didn't) for you.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI #Automation #Agents #ContentStrategy #Productivity
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Review Revolution: Why Code Review Is Now the Most Important Skill You Have</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Thu, 09 Apr 2026 10:54:23 +0000</pubDate>
      <link>https://dev.to/polivkadev/the-review-revolution-why-code-review-is-now-the-most-important-skill-you-have-1j1f</link>
      <guid>https://dev.to/polivkadev/the-review-revolution-why-code-review-is-now-the-most-important-skill-you-have-1j1f</guid>
      <description>&lt;p&gt;The PR came in on a Tuesday. Clean diff. All CI checks green. GitLab Duo had already run its pass — no obvious security anti-patterns, no style violations. The author was a solid mid-level developer, two years on the team.&lt;/p&gt;

&lt;p&gt;Then I asked: "Why did you choose this approach for the concurrency handling here?"&lt;/p&gt;

&lt;p&gt;Pause.&lt;/p&gt;

&lt;p&gt;"I think Claude Code suggested it and it seemed right."&lt;/p&gt;

&lt;p&gt;That was not the author lying or cutting corners. They had reviewed the output, it looked correct, and they shipped it. The code &lt;em&gt;was&lt;/em&gt; correct — at least for the happy path. What neither of us could answer in that moment: what happens when the lock acquisition times out? What is the failure mode if the downstream call is slow? Why this implementation over the simpler one?&lt;/p&gt;

&lt;p&gt;That is not a junior developer problem. That is a 2026 code review problem. And most teams are running their last checkpoint with 2019-era habits.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Before AI coding tools, the reviewer's job was well-defined: check the logic, catch edge cases, flag style violations, enforce team conventions. It was a hard job, but it had a reasonable assumption built in — the author understood what they wrote, because they wrote it.&lt;/p&gt;

&lt;p&gt;That assumption is gone.&lt;/p&gt;

&lt;p&gt;With Claude Code, GitHub Copilot, and similar tools in daily use, the reviewer now does two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the code itself&lt;/li&gt;
&lt;li&gt;Check whether the author understands the code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are different skills. The second is harder. And most review processes were not designed for it.&lt;/p&gt;

&lt;p&gt;The failure mode looks like this: a developer gets a requirement, pastes it into Claude Code, gets working code, reviews the output for obvious issues, and ships it. The PR reviewer checks the diff, finds nothing wrong, approves it. Neither person can articulate the performance characteristics, the failure modes, or the security implications of what was just merged.&lt;/p&gt;

&lt;p&gt;The data confirms this is not edge-case behavior. Veracode's 2026 analysis found that AI-generated code introduces security vulnerabilities in &lt;strong&gt;45% of cases&lt;/strong&gt; — in Java specifically, 72% of AI-generated code has at least one CWE hit. The Harness 2026 report found that organizations that adopted AI coding tools without updating their review processes saw a &lt;strong&gt;34% increase in production incidents&lt;/strong&gt; in 2025.&lt;/p&gt;

&lt;p&gt;This is not an argument against AI tools. It is an argument that the bar for code review just moved, and most teams have not moved with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Good Review Looks Like Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The New PR Description Standard
&lt;/h3&gt;

&lt;p&gt;The fastest way to know if a developer understood what they shipped is to make them explain it — in their own words, before anyone reviews the code.&lt;/p&gt;

&lt;p&gt;Add this to your team's PR template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## What does this do?
&lt;/span&gt;[Explain the intent — not the code, the problem being solved]

&lt;span class="gu"&gt;## How was this implemented?
&lt;/span&gt;[Explain the approach. If AI-generated, say so and explain why you accepted it.]

&lt;span class="gu"&gt;## Edge cases considered
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;[ ] [At least 3. Be specific.]

&lt;span class="gu"&gt;## How was this tested?
&lt;/span&gt;[If AI-generated tests were included, note which assertions were written by hand]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a developer cannot fill in "edge cases considered," they do not understand what they shipped. That is the signal — and it surfaces before the reviewer has read a single line of code.&lt;/p&gt;

&lt;p&gt;The best-performing engineering teams in 2026 are now requiring AI-generated PRs to include this explicitly: what the approach is, what edge cases were considered, and at least one self-generated test per non-trivial function. Not as bureaucracy — as a forcing function for comprehension.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Three Review Questions for AI-Generated Code
&lt;/h3&gt;

&lt;p&gt;When reviewing a PR that includes AI-generated sections — Claude Code output, GitLab Duo suggestions, Copilot completions — ask these three questions. Direct them at the code, not the author.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 1: What is this code optimizing for?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI-generated code is optimized for "solves the stated problem" and "compiles." It is not optimized for your specific performance SLAs, your team's error-handling conventions, or your domain's threat model.&lt;/p&gt;

&lt;p&gt;Look at the implementation choices. Is this optimized for throughput? For readability? For minimal memory allocation? Often the answer is: it's optimized for none of those, because the prompt didn't specify any of them. That is where the mismatch lives.&lt;/p&gt;

&lt;p&gt;Ask the author: "What constraint was this generated to solve?" If they cannot answer, the review should not proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 2: What happens when this fails?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI tends to generate happy-path code. Exception handling, circuit breakers, timeouts, and retry logic are frequently omitted or wrong.&lt;/p&gt;

&lt;p&gt;Look specifically at: what exceptions are caught and swallowed? What happens if the external call is slow or returns a 503? Is the failure mode recoverable or catastrophic? If a pod restarts mid-operation, what is the state of in-flight transactions?&lt;/p&gt;

&lt;p&gt;These are not exotic scenarios. These are Tuesday morning incidents. And they are the exact kind of thing AI code generation consistently misses, because the prompt that generated the code did not mention them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 3: Would a junior on this team be able to maintain this in 18 months?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI-generated code can be locally clever and globally incomprehensible. If the implementation relies on a pattern that is not in your team's standard vocabulary — and the PR description does not explain it — that is a code smell, even if the code is technically correct.&lt;/p&gt;

&lt;p&gt;Maintainability is not just about readability. It's about whether the next developer who touches this file will understand the &lt;em&gt;intent&lt;/em&gt; well enough to make safe changes. AI-generated code often optimizes for correctness at the cost of clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. GitLab Duo in the Review Flow
&lt;/h3&gt;

&lt;p&gt;GitLab Duo Code Review integrates at the merge request level and is genuinely useful — it catches style violations, obvious logic errors, and common security anti-patterns quickly.&lt;/p&gt;

&lt;p&gt;The key is using it correctly: treat Duo's pass as the &lt;strong&gt;mechanical layer&lt;/strong&gt;, not the full review. It handles the things a linter and a static analyzer would catch, at the speed of a bot.&lt;/p&gt;

&lt;p&gt;The human reviewer's job starts where Duo's ends: does the author understand what they shipped? Is the failure handling correct for this domain? Does the implementation reflect team conventions that are not captured in any linter rule?&lt;/p&gt;

&lt;p&gt;Duo is not a substitute for the three questions above. It is a first pass that frees you to ask better questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code Review as a Career Signal
&lt;/h2&gt;

&lt;p&gt;AI coding tools have done something unexpected to engineering career ladders: they made junior and mid-level code syntactically indistinguishable from senior code.&lt;/p&gt;

&lt;p&gt;A developer with 18 months of experience can now generate a working microservice — clean, organized, passing all automated checks — that looks like something a staff engineer would write. The output is impressive. The signal it sends about the developer's ability is... unclear.&lt;/p&gt;

&lt;p&gt;What AI cannot do is teach you to catch what it missed.&lt;/p&gt;

&lt;p&gt;Recognizing silent exception swallowing that will cause a service to fail silently under load — that requires having seen it happen. Noticing a data access pattern that will cause N+1 queries as the dataset grows — that requires understanding how your ORM generates SQL. Catching retry logic with no jitter that will thundering-herd your downstream service at exactly the moment you least want it to — that requires having been on-call when it happened to someone else.&lt;/p&gt;

&lt;p&gt;These instincts come from production experience. They come from reading incident reviews. They come from having opinions that are more specific than "this looks right."&lt;/p&gt;

&lt;p&gt;In 2026, the clearest seniority signal is not what you can generate — it is what you can review.&lt;/p&gt;

&lt;p&gt;If you are a mid-level developer trying to reach senior: become exceptional at code review. Not just at finding bugs, but at teaching through review. The comment that explains &lt;em&gt;why&lt;/em&gt; a pattern is dangerous, not just that it is. The question that forces the author to reason through failure modes they had not considered. That is senior engineering. AI cannot fake it.&lt;/p&gt;




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

&lt;p&gt;Before approving any PR that includes AI-generated code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comprehension checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Author can explain the approach in their own words&lt;/li&gt;
&lt;li&gt;[ ] PR description lists at least 3 edge cases, with specifics&lt;/li&gt;
&lt;li&gt;[ ] Implementation choice is justified (why this approach over alternatives)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure mode checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Exception handling is explicit — no empty catch blocks, no silent swallowing&lt;/li&gt;
&lt;li&gt;[ ] External calls have timeout and retry logic appropriate for the failure profile&lt;/li&gt;
&lt;li&gt;[ ] Failure modes are recoverable (or the PR explicitly notes they are not)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quality checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] At least one non-trivial test was written by the author, not generated&lt;/li&gt;
&lt;li&gt;[ ] Logging is present at failure points, not just on the happy path&lt;/li&gt;
&lt;li&gt;[ ] Any new DB query has been analyzed for N+1 risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Red flags that should pause approval:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I'm not sure why this works, but it does"&lt;/li&gt;
&lt;li&gt;100% test coverage where all tests are generated and assert the obvious&lt;/li&gt;
&lt;li&gt;An exception handler that logs and swallows without alerting&lt;/li&gt;
&lt;li&gt;An implementation that is clever in a way that is not explained anywhere&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The code review is the last checkpoint before production. For most of engineering history, it was also a reasonable proxy for whether the author understood what they wrote.&lt;/p&gt;

&lt;p&gt;That proxy is gone. The checkpoint remains.&lt;/p&gt;

&lt;p&gt;The teams that figure out how to update their review process — not abandon AI tools, not pretend nothing changed, but update the actual process — will have fewer incidents, faster onboarding, and a clearer signal for who is actually senior.&lt;/p&gt;

&lt;p&gt;The code review did not get less important. It got harder. And that is exactly the kind of harder that separates good engineers from the rest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What does your team's current process look like for reviewing AI-generated code? I'm curious what's working — share it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>career</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to create simple email form in your NodeJS website</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Sun, 10 Jul 2022 18:30:22 +0000</pubDate>
      <link>https://dev.to/polivkadev/how-to-create-simple-email-form-in-your-nodejs-website-3end</link>
      <guid>https://dev.to/polivkadev/how-to-create-simple-email-form-in-your-nodejs-website-3end</guid>
      <description>&lt;p&gt;Most webpages have some contact form, usually not very fancy and the backend behind it can be SalesForce, any other variation of lead generation software but usually it just sends email. &lt;/p&gt;

&lt;p&gt;Sending emails is not trivial, you need to have an SMTP server and that alone will add to your costs. Fortunately, we live in an age where everything is a service and API. There are tons of services that can send emails to you. My goal was to do it as cheaply as possible (preferably for free). I was expecting at most about 5 emails per day.&lt;/p&gt;

&lt;p&gt;After some research, I decided to use &lt;a href="https://www.sendinblue.com/" rel="noopener noreferrer"&gt;Send In Blue&lt;/a&gt;. Their free plan has 300 emails per day (more than enough for me), you do not need a credit card to sign up, no DNS shenanigans and their API is very straightforward. &lt;/p&gt;

&lt;h2&gt;
  
  
  API endpoint in NextJS
&lt;/h2&gt;

&lt;p&gt;NextJS offers a friendly and easy way to create API endpoints from where you can call any external APIs without actually exposing your secret keys to the browser. &lt;/p&gt;

&lt;p&gt;I got the API key from Send in Blue with the help of this awesome &lt;a href="https://help.sendinblue.com/hc/en-us/articles/209467485-What-s-an-API-key-and-how-can-I-get-mine-" rel="noopener noreferrer"&gt;help page&lt;/a&gt;, and add the key into my .env.local file.&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="nv"&gt;NEXT_PUBLIC_SEND_IN_BLUE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxapikeyxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also put the key into the Vercel Environment variables manager with the same name. &lt;/p&gt;

&lt;p&gt;I created a new file api/email.js from where I will send the email via Send In Blue API.&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="n"&gt;sendInBlueKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;NEXT_PUBLIC_SEND_IN_BLUE_KEY&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&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="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://api.sendinblue.com/v3/smtp/email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s1"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s1"&gt;'api-key'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sendInBlueKey&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;  
                &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
             &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="s2"&gt;"to"&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="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"myemail@ppolivka.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Pavel Polivka"&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="s2"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sb"&gt;`Contact Form Eamil From ${body.name}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="s2"&gt;"htmlContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sb"&gt;`
                &amp;lt;html&amp;gt;
                    &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
                    &amp;lt;body&amp;gt;
                        From: ${body.name} &amp;lt;br/&amp;gt;
                        Message: ${body.message} &amp;lt;br/&amp;gt;
                    &amp;lt;/body&amp;gt;
                &amp;lt;/html&amp;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;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;p&gt;From the form, I am getting the name and email of the sender and a message. I am sending that (the sender is the email from the form) to my email address so that I can reply to my email client. &lt;/p&gt;

&lt;h2&gt;
  
  
  Contact Form
&lt;/h2&gt;

&lt;p&gt;Then I created a contact form component. It's a simple form with three fields and the submit button. All the sending magic is happening in the handleSubmit function.&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;import&lt;/span&gt; &lt;span class="nc"&gt;TextField&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@mui/material/TextField'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@mui/material/Button'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'react'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Contact&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setSend&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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="nc"&gt;JSONdata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/api/email'&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;JSONdata&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;setSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;content&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="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Thank&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;contacting&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="n"&gt;onSubmit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextField&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"outlined"&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextField&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"outlined"&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextField&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Message"&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"outlined"&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;multiline&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"contained"&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"submit"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Send&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;/&amp;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;p&gt;I am using React Material UI, but you can use any UI or just normal inputs.&lt;/p&gt;

&lt;p&gt;This is all you need. Simple and elegant. It was done in an hour.&lt;/p&gt;

&lt;p&gt;Good luck coding. &lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Do you really need fancy productivity apps?</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Thu, 23 Jun 2022 19:40:09 +0000</pubDate>
      <link>https://dev.to/polivkadev/do-you-really-need-fancy-productivity-apps-3o7f</link>
      <guid>https://dev.to/polivkadev/do-you-really-need-fancy-productivity-apps-3o7f</guid>
      <description>&lt;p&gt;If you look at Medium, Twitter, or any other platform where many developers hang out you will see an obsession with productivity tools, apps, etc... Everybody is praising the latest shiny apps that will make you a productivity god.&lt;/p&gt;

&lt;p&gt;Did we ever stop and asked do we really need all of these apps? I have a busy work schedule and trying to manage that and still have some quality time with my family. So I naturally tried to be as much productive as I can. I downloaded almost all the apps that are in the talks. Tried them in the belief that they will help me with all my issues with time management, not enough time, and joggling all the tasks I need to complete.&lt;/p&gt;

&lt;p&gt;Well, I am here to report that it does not work. You actually lose a lot of time trying to use the tools.&lt;/p&gt;

&lt;p&gt;Take Obsidian for example. In theory, it's a very cool idea. Have all the notes in one place, interconnected with links between each other. Your second digital brain. Did somebody actually manage to keep a decent size library of all their notes with the links in all the places? When I am writing down a note I actually need to know what are my other existing notes to create a link to them. That seems stupid. Like really, really stupid. The chart showing the relationship between the notes looks impressive in the demos and videos but is it useful? What do you actually gain with it? Maybe just maybe, it's all just a bunch of fluff that will get you nothing.&lt;/p&gt;

&lt;p&gt;For me, the best solution for note-taking is basically a bunch of text files and an easy solution in full-text search in those. Wau, that sounds great and useful. What app does that? That is the beauty of this. You do not really need an app for this. It's part of your OS. You just put a bunch of text files in a folder and use Windows search or Apple Spotlight.&lt;/p&gt;

&lt;p&gt;Sure it's low tech. Sure it does not really work on phones. Do you really need all your work notes on the go? What is all the obsession with having everything synced and on the go? Do you really need your notes on how to restart a production server when you are grocery shopping? Do you need them on your phone at all? For productivity's sake, you should not really look at your phone when you work anyway. For the notes you need to have on the go you can use your phone notes app. What kind of notes do you need on the go? Maybe a list for your shopping, some measurements, etc... The notes app on your phone is more than capable of doing that.&lt;/p&gt;

&lt;p&gt;Another productivity giant is Notion. I never figured out what people really see on it. It's a bunch of templates for markdown files and kanban boards. Plus it's really sluggish and slow to use. Not to mention finicky. It's the most over-engineered solution for your notes and todos. What is the real need for kanban outside of some specific work use cases (and even there it's more or less questionable)? Do you really need to know that your window cleaning is not in progress? What is wrong with just a normal to-do list? If you have that many to-dos that you need to have them organized in some other way than just a plain list, you have a problem with delivering or overcommitting, not with the organization of your tasks.&lt;/p&gt;

&lt;p&gt;I tried a lot of different to-do list apps. Some better some worse. I always came back to just writing my todos down on paper. It forces you to keep the list small and manageable. Plus you know, it feels fancy.&lt;/p&gt;

&lt;p&gt;In the end, you use what you are comfortable with I will stick with my pile of text files, simple notes app, and paper.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>apps</category>
    </item>
    <item>
      <title>What is in your bag?</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Thu, 06 Jan 2022 19:25:40 +0000</pubDate>
      <link>https://dev.to/polivkadev/what-is-in-your-bag-1ijb</link>
      <guid>https://dev.to/polivkadev/what-is-in-your-bag-1ijb</guid>
      <description>&lt;p&gt;As a new year article, I decided to write up some words about what are the daily drivers I have in my bag. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bag
&lt;/h2&gt;

&lt;p&gt;I use a top-loaded HP bag for 15-inch laptops. My 17 inch Mac Book fits it well and it's pretty nice. But nothing special. &lt;/p&gt;

&lt;h2&gt;
  
  
  Laptops
&lt;/h2&gt;

&lt;p&gt;I have multiple laptops. For my work, I mostly use Macbook Pro 17" - 16GB RAM, 2019 model. I also have Macbook Air 13" M1 model I use for my personal projects. I also got a few Windows and Linux machines but those are rarely used.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Other electronics
&lt;/h2&gt;

&lt;p&gt;I carry Kindle Whitepaper 4 with me almost everywhere. I love reading.&lt;br&gt;
I also carry Jabra Evolve 75 I use it for work meetings. It's a really great headset. I also have Marshall Mode II true wireless for music and or audiobooks on the go. I love the Marshall sound. &lt;/p&gt;

&lt;p&gt;I also have some USB/USB-C cables and some USB-C dongles. Fully embracing that Mac dongle life. &lt;/p&gt;

&lt;h2&gt;
  
  
  Drug store
&lt;/h2&gt;

&lt;p&gt;I have deodorant in spray, hand sanitizer, mouth deodorant, and glass cleaning gel. Nothing special. &lt;/p&gt;

&lt;h2&gt;
  
  
  Various
&lt;/h2&gt;

&lt;p&gt;I have my Moleskin notebook, Parker pen, FFP2 respirator, chopsticks, and set of lockpicks. I have lockpicking as one of my hobbies I rarely use those but saved me calling locksmith more than once. &lt;/p&gt;




&lt;p&gt;If you like this article you can follow me on &lt;a href="https://twitter.com/pavel_polivka" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>gear</category>
    </item>
    <item>
      <title>React Componenets in Markdown articles</title>
      <dc:creator>Pavel Polívka</dc:creator>
      <pubDate>Wed, 15 Dec 2021 19:51:27 +0000</pubDate>
      <link>https://dev.to/polivkadev/react-componenets-in-markdown-articles-9bc</link>
      <guid>https://dev.to/polivkadev/react-componenets-in-markdown-articles-9bc</guid>
      <description>&lt;p&gt;As I decided to create my blog I choose Next.js as my framework of choice, as a backend I use markdown files stored in a git repo. &lt;/p&gt;

&lt;p&gt;To render my files I use &lt;a href="https://github.com/remarkjs/react-markdown" rel="noopener noreferrer"&gt;React Markdown&lt;/a&gt;. It's an awesome component that can render markdown files without any config.&lt;/p&gt;

&lt;p&gt;After a few posts, I figured out that I need some custom components rendered with my posts. One was for images, to make them zoomable for phones and the second one was a syntax highlighter for code snippets. &lt;/p&gt;

&lt;p&gt;React has a huge amount of components that can help us with both of these. I decided to use  &lt;a href="https://github.com/prc5/react-zoom-pan-pinch" rel="noopener noreferrer"&gt;react-zoom-pan-pinch&lt;/a&gt; for image zoom and &lt;a href="https://github.com/react-syntax-highlighter/react-syntax-highlighter" rel="noopener noreferrer"&gt;react-syntax-highlighter&lt;/a&gt; for syntax highlight.&lt;/p&gt;

&lt;p&gt;Ok. I have my components, not how to render them from our markdown files. Fortunately, React Markdown has us covered. It allows us to do a custom rendering of some tags. &lt;/p&gt;

&lt;p&gt;We can do something like this:&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;import&lt;/span&gt; &lt;span class="nc"&gt;ReactMarkdown&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'react-markdown'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Prism&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;SyntaxHighlighter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"react-syntax-highlighter"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;materialDark&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"react-syntax-highlighter/dist/cjs/styles/prism"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;TransformWrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TransformComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"react-zoom-pan-pinch"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactMarkdown&lt;/span&gt;
    &lt;span class="n"&gt;components&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="n"&gt;p&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="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}&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="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"img"&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="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TransformWrapper&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TransformComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;
                                &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                                &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sb"&gt;`${image.properties.src}`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                            &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;TransformComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;TransformWrapper&lt;/span&gt;&lt;span class="o"&gt;&amp;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;span class="c1"&gt;// Return default child if it's not an image&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;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;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;children&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="c1"&gt;// Removing "language-" because React-Markdown already added "language-"&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&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="n"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"language-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SyntaxHighlighter&lt;/span&gt;
                    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;materialDark&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&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="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;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="n"&gt;postData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentMarkdown&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ReactMarkdown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For images, we must hook into &lt;/p&gt;
&lt;p&gt; tag, as ReactMarkdown is wrapping images in paragraphs. So with a simple if we decide if the first child of the paragraph is an image. If so we replace the whole thing. &lt;/p&gt;

&lt;p&gt;For code, we are just replacing the whole code tag.&lt;/p&gt;




&lt;p&gt;If you like this article you can follow me on &lt;a href="https://twitter.com/pavel_polivka" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>markdown</category>
      <category>blog</category>
    </item>
  </channel>
</rss>
