<?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: Peter Kim Frank</title>
    <description>The latest articles on DEV Community by Peter Kim Frank (@peter).</description>
    <link>https://dev.to/peter</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1075%2F1c1975ce-97e8-401f-b99f-1ea88f9cae3e.jpeg</url>
      <title>DEV Community: Peter Kim Frank</title>
      <link>https://dev.to/peter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peter"/>
    <language>en</language>
    <item>
      <title>Converting old home movie DVDs into a private streaming site</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Wed, 08 Apr 2026 19:46:46 +0000</pubDate>
      <link>https://dev.to/peter/converting-old-home-movie-dvds-into-a-private-streaming-site-5bmb</link>
      <guid>https://dev.to/peter/converting-old-home-movie-dvds-into-a-private-streaming-site-5bmb</guid>
      <description>&lt;p&gt;A relative sent me a few DVDs containing &lt;em&gt;really old&lt;/em&gt; home videos that were at one point saved to DVDs.  The problem is, I don't own a DVD player, and neither do most people in 2026.  These discs were just sitting in a box, slowly becoming unplayable, holding memories nobody could actually watch (which is a real shame).&lt;/p&gt;

&lt;p&gt;So I bought a cheap USB DVD drive on Amazon (~$25, an Amicool A11), and figured I'd rip the discs and put the videos somewhere my family could get to them. I'm a vibe coder at this point so I used Claude Code and put together a workflow to rip the discs and upload the videos to private streaming site my whole family can pull up on their phones.&lt;/p&gt;

&lt;p&gt;Here's how we did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;On the software side, everything is free CLI tools installed through Homebrew: &lt;code&gt;ddrescue&lt;/code&gt; for ripping, &lt;code&gt;ffmpeg&lt;/code&gt; for video conversion, and &lt;code&gt;wrangler&lt;/code&gt; for uploading to Cloudflare. Claude Code handled the commands — I mostly described what I wanted and swapped discs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: make a safe copy of each disc
&lt;/h2&gt;

&lt;p&gt;The first thing Claude Code suggested was making a bit-perfect copy of each disc before doing anything else. That way even if a disc gets scratched later, we have an exact digital duplicate.&lt;/p&gt;

&lt;p&gt;The tool for this is &lt;code&gt;ddrescue&lt;/code&gt; (not &lt;code&gt;dd&lt;/code&gt;). The difference: if a disc has scratches or bad spots, &lt;code&gt;dd&lt;/code&gt; just fails. &lt;code&gt;ddrescue&lt;/code&gt; is smarter — it copies what it can, then goes back and retries the problem areas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ddrescue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were a couple of macOS-specific gotchas that Claude Code figured out along the way. The disc drive doesn't show up in &lt;code&gt;diskutil list&lt;/code&gt; like a normal drive — you need &lt;code&gt;drutil status&lt;/code&gt; to detect it. And ddrescue needs to be told the exact disc size, otherwise it thinks there's 9,223 petabytes of data to read (basically infinity). The size comes from the block count that &lt;code&gt;drutil&lt;/code&gt; reports.&lt;/p&gt;

&lt;p&gt;We wrapped all of this into a script so I could just run &lt;code&gt;./rip.sh disc-01&lt;/code&gt;, wait about 10 minutes, swap the disc, and repeat. Each disc produced a ~4.3 GB &lt;code&gt;.iso&lt;/code&gt; file — a perfect clone that I can keep forever as an archive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: convert to something playable
&lt;/h2&gt;

&lt;p&gt;Mount the ISO and you find a &lt;code&gt;VIDEO_TS&lt;/code&gt; folder full of &lt;code&gt;.VOB&lt;/code&gt; files. That's the old DVD-Video format — fine for DVD players, useless for phones and browsers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ffmpeg&lt;/code&gt; converts each clip to a modern H.264 MP4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; VTS_01_1.VOB &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="se"&gt;\&lt;/span&gt;
  clip-01.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't know any of these flags — Claude Code picked the settings and explained why each one matters (the &lt;code&gt;-movflags +faststart&lt;/code&gt; one moves metadata to the front of the file so browsers can stream it without downloading the whole thing first). My discs had 13 to 21 clips each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three: put it online
&lt;/h2&gt;

&lt;p&gt;You could upload these to YouTube (unlisted), Google Drive, or Dropbox and call it a day. But I liked the idea of having my own thing — files on storage I control, a simple site I can customize, no platform deciding to compress my videos or change their sharing features.&lt;/p&gt;

&lt;p&gt;Claude Code set up the hosting on Cloudflare, which turned out to be free for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare R2&lt;/strong&gt; stores the video files. It's object storage, like a hard drive in the cloud. You upload files with a CLI command and they just sit there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; hosts a simple website — one HTML file with a grid of video thumbnails.&lt;/li&gt;
&lt;li&gt;A small &lt;strong&gt;Pages Function&lt;/strong&gt; connects the two, serving videos from R2 when you click play.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The upload was just a loop running &lt;code&gt;wrangler r2 object put&lt;/code&gt; for each clip. Claude Code handled the commands while I did other things.&lt;/p&gt;

&lt;p&gt;The one step I had to do manually in the Cloudflare dashboard was connecting the R2 bucket to the Pages site. Under the project's Settings &amp;gt; Bindings, I added an R2 binding so the site's code could actually read from the bucket. Took about 30 seconds but it's not something the CLI can do yet.&lt;/p&gt;

&lt;p&gt;To lock it down to family only, there's &lt;strong&gt;Cloudflare Access&lt;/strong&gt; — you add email addresses of people who should have access, and everyone else gets a login screen. Also free for small groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gallery
&lt;/h2&gt;

&lt;p&gt;The site itself is a single HTML page. No React, no build step. Just a dark grid of thumbnails.&lt;/p&gt;

&lt;p&gt;Claude Code added a couple of nice touches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lightweight thumbnails&lt;/strong&gt; — instead of loading 47 video players on page load, each clip shows a tiny JPEG thumbnail (~5 KB) with a play button. Click it and the actual video loads. The page loads in under a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hover preview&lt;/strong&gt; — move your mouse across any thumbnail and it scrubs through the video, YouTube-style. This is done with sprite sheets — one image per clip containing 20 frames tiled horizontally (~40 KB each). No video data loads until you actually click play.&lt;/p&gt;

&lt;p&gt;There's a small progress bar at the bottom of each thumbnail as you scrub so you can tell where you are in the clip.&lt;/p&gt;

&lt;p&gt;The video manifest is just a JavaScript object that maps disc names to clip counts. When I rip a new disc, I add one line and redeploy.&lt;/p&gt;

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

&lt;p&gt;Essentially nothing. Cloudflare R2 doesn't charge for bandwidth — when family streams a video, there's no per-GB fee. Storage is free up to 10 GB, and my 3-disc archive is about 8 GB of converted video. Even if I rip every disc in the box I'd be well under $1/month. For a family casually rewatching old home videos, this is basically a free setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rip script
&lt;/h2&gt;

&lt;p&gt;Here's the script Claude Code and I ended up with. It auto-detects the disc, figures out the size, and runs a two-pass rip:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-lt&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;disc-label&amp;gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;RAW_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/raw"&lt;/span&gt;
&lt;span class="nv"&gt;LOG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/logs"&lt;/span&gt;

&lt;span class="nv"&gt;DISC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;drutil status 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Name:"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $NF}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No disc detected. Insert a disc and try again."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;BLOCKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;drutil status 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Space Used:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*blocks:[[:space:]]*//'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DISC_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; BLOCKS &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2048&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

diskutil unmountDisk &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;ddrescue &lt;span class="nt"&gt;-b&lt;/span&gt; 2048 &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC_SIZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RAW_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iso"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log"&lt;/span&gt;
ddrescue &lt;span class="nt"&gt;-b&lt;/span&gt; 2048 &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC_SIZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; 3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RAW_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iso"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done! &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RAW_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iso"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pop a disc in, run &lt;code&gt;./rip.sh disc-04&lt;/code&gt;, swap, repeat. About 10 minutes per disc.&lt;/p&gt;




&lt;p&gt;The whole project took about an hour of actual thinking work, and a few hours of waiting for discs to rip and videos to upload while I did other things. Claude Code handled the parts I didn't know — the ffmpeg flags, the Cloudflare Pages Function for video seeking, the sprite sheet math — and I handled the part it couldn't: physically swapping discs.&lt;/p&gt;

&lt;p&gt;My mom can now watch her old home videos on her phone. That alone was worth it.  And I can rest easy knowing these family memories are safely stored in the cloud.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>cloudflare</category>
      <category>dvd</category>
      <category>ripping</category>
    </item>
    <item>
      <title>Sorting algorithms visualized</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Mon, 23 Feb 2026 19:48:11 +0000</pubDate>
      <link>https://dev.to/peter/sorting-algorithms-visualized-5oh</link>
      <guid>https://dev.to/peter/sorting-algorithms-visualized-5oh</guid>
      <description>&lt;p&gt;Wanted an excuse to play with the new &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-1-pro/" rel="noopener noreferrer"&gt;Gemini 3.1 Pro&lt;/a&gt; so I vibe-coded this sorting visualization playground and deployed it to Cloud Run.&lt;/p&gt;

&lt;p&gt;I always thought &lt;a href="https://www.youtube.com/watch?v=kPRA0W1kECg" rel="noopener noreferrer"&gt;these sorts of videos were fun&lt;/a&gt; so it felt cool to sort of build my own.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://sorting-visualizer-584800428475.us-west1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




</description>
      <category>sorting</category>
      <category>visualization</category>
      <category>data</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Setting up a public URL that flashes my office lights</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Mon, 05 Jan 2026 18:38:28 +0000</pubDate>
      <link>https://dev.to/peter/setting-up-a-public-url-that-flashes-my-office-lights-218i</link>
      <guid>https://dev.to/peter/setting-up-a-public-url-that-flashes-my-office-lights-218i</guid>
      <description>&lt;p&gt;I've been running Home Assistant on a Raspberry Pi for over two years. It controls my Hue lights, Zigbee devices, the usual stuff. It's always been a local network thing.&lt;/p&gt;

&lt;p&gt;As every vibecoder knows, you've got it working on &lt;a href="https://localhost:3000" rel="noopener noreferrer"&gt;https://localhost:3000&lt;/a&gt; but that doesn't mean you can show your friends type of thing.&lt;/p&gt;

&lt;p&gt;I had this little home network but I never wanted to expose it to the broader internet.&lt;/p&gt;

&lt;p&gt;This past weekend, I spun up a $4/month DigitalOcean droplet to run &lt;a href="https://ntfy.sh" rel="noopener noreferrer"&gt;ntfy&lt;/a&gt;, a self-hosted push notification service. The idea was to give various automations a way to send push notifications to my phone.&lt;/p&gt;

&lt;p&gt;Then I realized: now I've got a droplet which can serve as my internet-facing endpoint.  That means I can probably gateway to trigger things on my home network via HomeAssistant&lt;/p&gt;

&lt;p&gt;So, I now have a URL that &lt;em&gt;anyone with a token can hit&lt;/em&gt; to flash my office lights red - pretty much an IRL ping that something needs my attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;My Pi is behind my home router. I don't want to port-forward or expose Home Assistant directly. But I want to trigger it from the internet.&lt;/p&gt;

&lt;p&gt;The droplet solves this. It's public-facing. The question is: how do I get from the droplet to my Pi securely?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Tailscale
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tailscale.com" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; creates a mesh VPN between devices. Install it on both the droplet and the Pi, and they can talk to each other using private IPs (like &lt;code&gt;100.x.x.x&lt;/code&gt;) - no port forwarding needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Droplet (public) → Tailscale → Pi (private) → Home Assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Claude Code Built
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; to wire this up.  My key insight was that I could simply give Claude Code SSH access to both my Pi and Droplet and let it handle a lot of the rest.&lt;/p&gt;

&lt;p&gt;I described what I wanted, and it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSHed into my Pi and queried Home Assistant to find my light entity IDs&lt;/li&gt;
&lt;li&gt;Wrote a bash script that flashes the lights red, then restores the previous color&lt;/li&gt;
&lt;li&gt;Installed Tailscale on both the Pi and droplet&lt;/li&gt;
&lt;li&gt;Generated SSH keys so the droplet can run commands on the Pi&lt;/li&gt;
&lt;li&gt;Created a Flask webhook with token-based auth&lt;/li&gt;
&lt;li&gt;Set up nginx to route requests&lt;/li&gt;
&lt;li&gt;Created systemd services so everything survives reboots&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing took maybe 20 minutes. Most of that was waiting for apt to install packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request: GET /flash-peter-office-lights?auth_token=xxx
                    ↓
            Cloudflare (HTTPS)
                    ↓
            DigitalOcean Droplet
            nginx → Flask (port 5000)
                    ↓
            Tailscale (100.x.x.x)
                    ↓
            Raspberry Pi
            SSH → flash_lights.sh
                    ↓
            Home Assistant API
                    ↓
            Lights flash red → restore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Flash Script
&lt;/h2&gt;

&lt;p&gt;The tricky part is restoring the lights to their previous state. Home Assistant lights can be in different color modes, so the script saves the current state before flashing:&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;# Save current state&lt;/span&gt;
&lt;span class="nv"&gt;STATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://localhost:8123/api/states/light.office"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;WAS_ON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$STATE&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.state'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BRIGHTNESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$STATE&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.attributes.brightness // 255'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;XY_X&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$STATE&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.attributes.xy_color[0] // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;XY_Y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$STATE&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.attributes.xy_color[1] // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Flash red&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8123/api/services/light/turn_on"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"entity_id": "light.office", "rgb_color": [255, 0, 0], "brightness": 255}'&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;1

&lt;span class="c"&gt;# Restore&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8123/api/services/light/turn_on"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;entity_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;light.office&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;brightness&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$BRIGHTNESS&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;xy_color&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: [&lt;/span&gt;&lt;span class="nv"&gt;$XY_X&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$XY_Y&lt;/span&gt;&lt;span class="s2"&gt;]}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First version only saved brightness. When I told Claude Code "the lights aren't going back to where they were," it figured out the issue and added the &lt;code&gt;xy_color&lt;/code&gt; handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Webhook (Flask)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_tokens&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/root/webhooks/tokens.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/flash-peter-office-lights&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing auth_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;

    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_tokens&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;

    &lt;span class="c1"&gt;# SSH to Pi via Tailscale and run the flash script
&lt;/span&gt;    &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh -i /root/.ssh/pi_key peter@100.x.x.x &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/peter/flash_lights.sh&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flashed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokens live in a JSON file:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alice-token-123"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-05"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bob-token-456"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-05"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each person gets their own token. Revoke access by deleting their entry.&lt;/p&gt;

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

&lt;p&gt;Now that the plumbing exists, I can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different colors for different sources&lt;/strong&gt; - blue for Slack, green for family texts, red for emergencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack slash command&lt;/strong&gt; - &lt;code&gt;/flash-peter&lt;/code&gt; for coworkers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS Shortcut&lt;/strong&gt; - one-tap button for my wife&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; - prevent abuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt; - who flashed and when&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to build something similar, the pieces are: Raspberry Pi (or another device to run HomeAssistant), a cheap VPS with Tailscale on both ends, and some basic Python/bash (in my case, written by Claude).&lt;/p&gt;

</description>
      <category>homeassistant</category>
      <category>raspberr</category>
      <category>tailscale</category>
      <category>automation</category>
    </item>
    <item>
      <title>Welcome Thread</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Fri, 19 Sep 2025 19:17:15 +0000</pubDate>
      <link>https://dev.to/peter/welcome-thread-n3m</link>
      <guid>https://dev.to/peter/welcome-thread-n3m</guid>
      <description>&lt;p&gt;Welcome!  What have you built recently?&lt;/p&gt;

</description>
      <category>welcome</category>
    </item>
    <item>
      <title>The new PostHog.com is pretty amazing ... most distinctive landing page I've seen in a long time.</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Thu, 11 Sep 2025 13:08:04 +0000</pubDate>
      <link>https://dev.to/peter/the-new-posthogcom-is-pretty-amazing-most-distinctive-landing-page-ive-seen-in-a-long-1kna</link>
      <guid>https://dev.to/peter/the-new-posthogcom-is-pretty-amazing-most-distinctive-landing-page-ive-seen-in-a-long-1kna</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://posthog.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fposthog.com%2Fimages%2Fog%2Fdefault.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://posthog.com/" rel="noopener noreferrer" class="c-link"&gt;
            PostHog – We make dev tools for product engineers
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            All your developer tools in one place. PostHog gives engineers everything to build, test, measure, and ship successful products faster. Get started free.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fposthog.com%2Ffavicon-32x32.png%3Fv%3D6e5ac8d4a5b381b5caa29396fbf7c955"&gt;
          posthog.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>design</category>
      <category>discuss</category>
      <category>startup</category>
    </item>
    <item>
      <title>What tools are you using to vibe code?</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Tue, 12 Aug 2025 00:07:53 +0000</pubDate>
      <link>https://dev.to/peter/what-tools-are-you-using-to-vibe-code-2oe1</link>
      <guid>https://dev.to/peter/what-tools-are-you-using-to-vibe-code-2oe1</guid>
      <description>&lt;p&gt;Share the tools, platforms, technologies that you're using as you vibe code.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
    </item>
    <item>
      <title>Tip: ask your agent to create a /debug page</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Fri, 11 Jul 2025 23:11:33 +0000</pubDate>
      <link>https://dev.to/peter/tip-ask-your-agent-to-create-a-debug-page-57ek</link>
      <guid>https://dev.to/peter/tip-ask-your-agent-to-create-a-debug-page-57ek</guid>
      <description>&lt;p&gt;Have been using this "trick" more and more frequently.&lt;/p&gt;

&lt;p&gt;When you're running into trouble articulating what's going right or wrong when working with a coding assistant/agent, consider asking it to build a &lt;code&gt;/debug&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;Doesn’t need to be fancy. Just a simple &lt;strong&gt;live and visual&lt;/strong&gt; canvas that shows: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the agent &lt;em&gt;thinks&lt;/em&gt; it’s doing
&lt;/li&gt;
&lt;li&gt;what inputs it’s working with
&lt;/li&gt;
&lt;li&gt;what tools are firing (or not)
&lt;/li&gt;
&lt;li&gt;any recent errors or outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are other ways to do this sort of thing (browser/server logs, MCP with tighter feedback loops, etc), but this has been a quick/dirty trick that has saved me some frustrating debugging sessions.&lt;/p&gt;

&lt;p&gt;Especially good when something’s &lt;em&gt;almost&lt;/em&gt; working and you can’t quite tell why.&lt;/p&gt;

</description>
      <category>vibecode</category>
      <category>beginners</category>
      <category>ai</category>
      <category>css</category>
    </item>
    <item>
      <title>Join the World's Largest Hackathon: $1 Million in Prizes 💸</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Fri, 30 May 2025 19:27:41 +0000</pubDate>
      <link>https://dev.to/devteam/join-the-worlds-largest-hackathon-1-million-in-prizes-3hfh</link>
      <guid>https://dev.to/devteam/join-the-worlds-largest-hackathon-1-million-in-prizes-3hfh</guid>
      <description>&lt;p&gt;Our friends at &lt;a href="https://bolt.new/" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt; just officially kicked off the &lt;a href="https://hackathon.dev/" rel="noopener noreferrer"&gt;World's Largest Hackathon&lt;/a&gt;, an awesome competition with &lt;strong&gt;over $1,000,000 in prizes&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;We love that this hackathon is breaking down barriers and welcoming new participants into the ecosystem.  If you’re not yet familiar with Bolt, it’s a powerful AI-platform that enables &lt;em&gt;anyone&lt;/em&gt; with an idea to build production-ready applications without traditional coding skills. &lt;/p&gt;

&lt;p&gt;Whether you're a seasoned developer or a first-time entrepreneur with a vision, we hope you give this hackathon a try! &lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h2&gt;
  
  
  How To Participate
&lt;/h2&gt;

&lt;p&gt;Click the link below for all the details on how to get started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://worldslargesthackathon.devpost.com/" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Join the World's Largest Hackathon&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fun Fact: Everyone who signs up for the hackathon will receive a builder pack with thousands of credits to different services - AI voice, UI gen, infra, you name it!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the Hackathon Kickoff Video:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KY8xaxkQz0w"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;


&lt;/div&gt;


&lt;h2&gt;
  
  
  Community Support
&lt;/h2&gt;

&lt;p&gt;The main Hackathon entry process will take place via DevPost, but we’ll be supporting the World’s Largest Hackathon in a variety of ways!&lt;/p&gt;

&lt;h3&gt;
  
  
  Livestream Events
&lt;/h3&gt;

&lt;p&gt;Starting next week, we'll be simulcasting as many hackathon-related livestreams as possible directly on the DEV homepage. Here are the first three sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain Expertise with Entri&lt;/strong&gt; - June 2, 2025 at 9:00AM PT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;So what is an API anyways? With Pica&lt;/strong&gt; - June 3, 2025 at 9:00AM PT
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building Conversational Video Agents with Tavus and Bolt&lt;/strong&gt; - June 4, 2025 at 9:00AM PT&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Judging
&lt;/h3&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/jess"&gt;@jess&lt;/a&gt; will be bringing her judging experience from &lt;a href="https://dev.to/challenges"&gt;DEV Challenges&lt;/a&gt; to the event and will be serving as an official judge for the hackathon! She can't wait to check out all the innovative projects that emerge from this event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Contest Coming Soon
&lt;/h3&gt;

&lt;p&gt;We'll also be launching a &lt;strong&gt;writing contest specifically for World’s Largest Hackathon participants&lt;/strong&gt; later this month, so stay tuned for details on how you can share your building journey and compete for exclusive DEV badges!&lt;/p&gt;




&lt;p&gt;What are you planning to build? Drop a comment and let the community know! 👇&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>vibecoding</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Apple Magic Keyboard (USB-C) function keys not working</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Wed, 02 Apr 2025 10:16:00 +0000</pubDate>
      <link>https://dev.to/peter/apple-magic-keyboard-usb-c-function-keys-not-working-3kag</link>
      <guid>https://dev.to/peter/apple-magic-keyboard-usb-c-function-keys-not-working-3kag</guid>
      <description>&lt;p&gt;&lt;em&gt;Note: I'm basically writing this post for Google because I couldn't find useful/relevant results when I was searching myself...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Macbook Pro running Sonoma 14.6.1 on Apple Silicon&lt;/li&gt;
&lt;li&gt;New keyboard is model A3118 with USB-C&lt;/li&gt;
&lt;li&gt;Old keyboard is model A2449 with Lightning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've been using a Magic Keyboard with Touch ID (lightning cable) for many, many, many years. It’s been rock-solid the entire with time — no issues, always reliable. But recently, a key on my Lightning version stuck/broke so I decided to get a replacement.  I chose a new model (A3118) with a USB-C connector instead of Lightning. &lt;/p&gt;

&lt;p&gt;Seemed like a minor upgrade—same keyboard, just with the modern connector, right?&lt;/p&gt;

&lt;p&gt;Not quite.&lt;/p&gt;

&lt;p&gt;Right out of the box, the USB-C version had issues. It connected fine, typed fine, but none of the system-level features worked. The Touch ID sensor didn’t respond. The function keys—brightness, volume, media—didn’t do anything. I toggled every keyboard setting I could find in System Settings. Nothing changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything worked as normal&lt;/strong&gt; on my built-in native keyboard on my Macbook, so I knew it was a firmware/connection issue and not something system wide.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Flku9bxx3mu4fu1e1u9u6.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.amazonaws.com%2Fuploads%2Farticles%2Flku9bxx3mu4fu1e1u9u6.png" alt=" " width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Yes, I tried toggling the Function Keys in Keyboard Settings&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Restarted my MacBook Pro)&lt;/li&gt;
&lt;li&gt;Connected via USB-C directly&lt;/li&gt;
&lt;li&gt;Paired via Bluetooth&lt;/li&gt;
&lt;li&gt;Used it in wired-only mode&lt;/li&gt;
&lt;li&gt;Reset keyboard settings and Bluetooth modules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still broken.&lt;/p&gt;

&lt;p&gt;To double-check it wasn’t my machine, I plugged in the old Lightning version. Everything worked perfectly—Touch ID, function keys, all the macOS integrations I’d come to expect.&lt;/p&gt;

&lt;p&gt;I brought my Macbook Pro along with the new USB-C keyboard to the Apple Store. They tested it and saw the same issues.  They then brought out their store tester keyboards (both USB-C and Lightning) and were able to confirm the issue -- demonstrating that it wasn't a specific faulty unit, but a broader issue.  Lightning worked without issue, USB-C has the broken function keys.&lt;/p&gt;

&lt;p&gt;So, yeah... if you’re considering upgrading to the new USB-C Magic Keyboard with Touch ID: maybe wait or do a bit more research. There seems to be a firmware or macOS-level compatibility issue that breaks key features.&lt;/p&gt;

&lt;p&gt;Hope this helps the next person avoid the painful Google search + troubleshooting issues.&lt;/p&gt;

</description>
      <category>apple</category>
      <category>hardware</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>check out my new landing page @ https://localhost:3000 and please send me feedback</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Tue, 01 Apr 2025 14:13:25 +0000</pubDate>
      <link>https://dev.to/peter/check-out-my-new-landing-page-httpslocalhost3000-and-please-send-me-feedback-25dm</link>
      <guid>https://dev.to/peter/check-out-my-new-landing-page-httpslocalhost3000-and-please-send-me-feedback-25dm</guid>
      <description></description>
    </item>
    <item>
      <title>Claude 3.7 Released</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Mon, 24 Feb 2025 20:09:37 +0000</pubDate>
      <link>https://dev.to/peter/claude-37-released-e95</link>
      <guid>https://dev.to/peter/claude-37-released-e95</guid>
      <description>&lt;p&gt;Earlier today, &lt;a href="https://www.anthropic.com/news/claude-3-7-sonnet" rel="noopener noreferrer"&gt;Anthropic released&lt;/a&gt; their new model -- Claude 3.7.&lt;/p&gt;

&lt;p&gt;Anthropic's Claude Sonnet 3.5 has been one of my most-used models so I'm excited to try it out.&lt;/p&gt;

&lt;p&gt;Seems like a really strong model for SWE use cases.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fs7f0bomk618h1f3vql6v.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.amazonaws.com%2Fuploads%2Farticles%2Fs7f0bomk618h1f3vql6v.png" alt="SWE verified stats" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>anthropic</category>
    </item>
    <item>
      <title>Generating ~450 images for $0.50</title>
      <dc:creator>Peter Kim Frank</dc:creator>
      <pubDate>Thu, 20 Feb 2025 15:23:42 +0000</pubDate>
      <link>https://dev.to/peter/generating-450-images-for-050-1elj</link>
      <guid>https://dev.to/peter/generating-450-images-for-050-1elj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Gallery link: &lt;a href="https://pkf-ai-gallery.pages.dev/" rel="noopener noreferrer"&gt;https://pkf-ai-gallery.pages.dev/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been getting more and more interested in the &lt;a href="https://bittensor.com/" rel="noopener noreferrer"&gt;Bittensor ecosystem&lt;/a&gt;, a decentralized, open-source network interconnected machine learning models.&lt;/p&gt;

&lt;p&gt;One of the more interesting "subnets" in the network is &lt;a href="https://chutes.ai" rel="noopener noreferrer"&gt;Chutes.ai&lt;/a&gt; which provides Serverless AI Compute across a bunch of different LLMs, image models, etc.  I had listened to them in a &lt;a href="https://bittensor.guru/s2e6-chutesai-subnet-64-w-namoray-and-jon-durbin" rel="noopener noreferrer"&gt;recent podcast interview&lt;/a&gt; and was looking for an excuse to play around.&lt;/p&gt;

&lt;p&gt;I asked DeepSeek-R1 to come up with 50 imaginative prompts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;A celestial cathedral floating atop a spiraling nebula, its stained-glass windows depicting constellations come to life, gilded arches entwined with ivy made of starlight, surrounded by floating orbs of liquid mercury&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;An opulent steampunk airship shaped like a mechanical peacock, its feathers crafted from interlocking brass gears and glowing amber lenses, hovering above a fog-shrouded Victorian metropolis illuminated by gaslamps&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;A surreal garden where trees are composed of cascading sapphire ribbons, their roots embedded in pools of liquid gold, guarded by stone serpents with eyes of smoldering opal under a twilight sky streaked with auroras&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;47 more...&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then ran those through 9 different image models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dreamshaper XL 1.0&lt;/li&gt;
&lt;li&gt;Flux 1 Dev&lt;/li&gt;
&lt;li&gt;Flux 1 Schnell&lt;/li&gt;
&lt;li&gt;JuggernautXL&lt;/li&gt;
&lt;li&gt;Ostris Flex 1 Alpha&lt;/li&gt;
&lt;li&gt;Playground v2.5&lt;/li&gt;
&lt;li&gt;Psychedelic Trees&lt;/li&gt;
&lt;li&gt;Realistic Vision v5.1&lt;/li&gt;
&lt;li&gt;Shitao Omnigen v1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in all, it cost about $0.50 (~$0.001 per image), which I paid for using $TAO, the native currency of Bittensor.&lt;/p&gt;

&lt;p&gt;I used Cursor's new-ish Agent mode to write the Python code to make all of this possible.  The entire project took about ~15-20 minutes of tinkering around in Cursor, and then an hour or two to generate all of the images (which I just let run before going to bed).&lt;/p&gt;

&lt;p&gt;I then took the list of prompts and image directory and (again) had Cursor generate a gallery that I uploaded to Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can view the final gallery here: &lt;a href="https://pkf-ai-gallery.pages.dev/" rel="noopener noreferrer"&gt;https://pkf-ai-gallery.pages.dev/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Overall, this was a fun little project that has been made possible / much easier through the advent of lower-cost AI models, and code workflow assistants like Copilot, Cursor, Windsurf, Replit Agent, Q Developer, etc.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
