<?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: Sandro 🦖☄️</title>
    <description>The latest articles on DEV Community by Sandro 🦖☄️ (@sgumz).</description>
    <link>https://dev.to/sgumz</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%2F538857%2Ffeefe1c8-6595-4375-a30b-c69c49903d0b.jpg</url>
      <title>DEV Community: Sandro 🦖☄️</title>
      <link>https://dev.to/sgumz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sgumz"/>
    <language>en</language>
    <item>
      <title>Converting RAW to HEIC inside Apple Photos: App Intents, HDR gain maps, and never losing a metadata field</title>
      <dc:creator>Sandro 🦖☄️</dc:creator>
      <pubDate>Thu, 25 Jun 2026 06:30:58 +0000</pubDate>
      <link>https://dev.to/sgumz/converting-raw-to-heic-inside-apple-photos-app-intents-hdr-gain-maps-and-never-losing-a-metadata-1in7</link>
      <guid>https://dev.to/sgumz/converting-raw-to-heic-inside-apple-photos-app-intents-hdr-gain-maps-and-never-losing-a-metadata-1in7</guid>
      <description>&lt;p&gt;I built an iOS/iPadOS/macOS app called RawToHEIC. The pitch is one sentence: you select RAW photos in the Photos app, tap Share, pick "Convert RAW to HEIC," and a few seconds later your library has HEICs that are up to 10× smaller — with every edit, album, favorite, and GPS coordinate intact.&lt;/p&gt;

&lt;p&gt;That sentence took an order of magnitude more engineering than it sounds like. This post is the part developers actually want: where the hard parts were, the decisions I'd defend, and the platform behavior that cost me days. If you're building anything that touches PhotoKit, ImageIO, or App Intents, some of this will save you time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an App Intent, and specifically not a Share Extension
&lt;/h2&gt;

&lt;p&gt;The first real decision shaped everything else. There are two obvious ways to put "do something with these photos" into the Photos Share sheet: a classic Share Extension, or an App Intent.&lt;/p&gt;

&lt;p&gt;A Share Extension feels like the natural choice. It's the old, well-trodden path. But it has a fatal limitation for this particular app: extensions run in a sandboxed, memory-constrained host process with a restricted permission surface. Crucially, the operation at the heart of this app — deleting the user's RAW originals from their Photos library after a successful conversion — requires &lt;code&gt;deleteAssets&lt;/code&gt;, and that requires app-level Photos permissions that an extension host doesn't grant the way a full app does.&lt;/p&gt;

&lt;p&gt;App Intents are the answer. An App Intent invoked from the Share sheet runs with the parent app's identity and permissions. So the architecture became: the intent is a thin entry point that collects the selected assets and hands the actual work to the main app, which has the permissions to write and (with confirmation) delete in the library.&lt;/p&gt;

&lt;p&gt;This also solved a memory problem before it started. Intent-hosting UI on iOS lives under tight memory limits — the inline "Snippet" you show in the Share sheet has to stay lean. So conversion does not run in the intent context. The intent shows the estimate and confirmation Snippet; the heavy lifting — decode, encode, library writes — happens in the main app process where there's room to breathe. The design rule I held myself to: the Snippet stays within about 10 MB over baseline, and no pixel-crunching happens inside it.&lt;/p&gt;

&lt;p&gt;One target, SwiftUI multiplatform, no Mac Catalyst, no separate extension target. One &lt;code&gt;AppIntent&lt;/code&gt; as the front door. If you're weighing Share Extension vs. App Intent for anything that needs real library mutation, that permission asymmetry is the deciding factor, and it's not well advertised.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data-integrity invariants, because this app deletes people's photos
&lt;/h2&gt;

&lt;p&gt;Here's the thing that kept me up at night. The entire value proposition is reclaiming storage, which means deleting RAW originals. An app that occasionally loses someone's photo is not a storage app, it's a tragedy generator. So before I wrote much conversion code, I wrote down the invariants the system must never violate, and I enforce them with &lt;code&gt;precondition&lt;/code&gt; in debug builds and with unit tests.&lt;/p&gt;

&lt;p&gt;The non-negotiable ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A RAW is never deleted before its HEIC is successfully written to Photos &lt;em&gt;and&lt;/em&gt; verified by a synchronous &lt;code&gt;PHAsset.fetchAssets&lt;/code&gt; lookup.&lt;/strong&gt; Not "the write returned success." Verified present, by fetching it back. The order is sacred: convert, write, fetch-to-confirm, &lt;em&gt;then&lt;/em&gt; the original becomes eligible for deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deletes are batched.&lt;/strong&gt; Exactly one &lt;code&gt;deleteAssets&lt;/code&gt; call per batch, never one call per asset. This matters both for the UX (a single system confirmation prompt instead of death-by-a-thousand-dialogs) and for atomicity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the user didn't opt into deletion, the delete code path is not merely skipped — it's unreachable.&lt;/strong&gt; Guarded by &lt;code&gt;precondition&lt;/code&gt;. "Off" doesn't mean "we chose not to call it." It means there is no live path to the deletion call at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temp HEIC files are moved, not copied, into Photos, and unlinked in the same change block.&lt;/strong&gt; No orphaned temp files, no double the disk usage mid-flight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the app is killed mid-batch, partial deletion is impossible.&lt;/strong&gt; Because deletion only happens in one batched call after all conversions have drained and been verified, a crash at any earlier point leaves every original untouched. Even termination &lt;em&gt;during&lt;/em&gt; the system delete prompt results in no deletion, because PhotoKit cancels unresolved change requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a subtler one that took a real-world failure to appreciate. Re-linking a converted HEIC into its original album is a separate PhotoKit change request, and it can fail independently. The rule: &lt;strong&gt;if the album re-link fails, keep the new HEIC, flag the asset as a partial restore, and do not delete the original — regardless of the batch's delete setting.&lt;/strong&gt; A photo that lost its album home is not a successful conversion, so its original earns a stay of execution. Better a duplicate than an orphan.&lt;/p&gt;

&lt;p&gt;If you take one thing from this section: when your app does something irreversible to user data, write the safety invariants down as explicit, testable assertions &lt;em&gt;first&lt;/em&gt;, in plain language, before you write the feature. They become both your spec and your regression net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping everything: edits, albums, favorites, locations
&lt;/h2&gt;

&lt;p&gt;"Convert the photo" is the easy 20%. "Convert the photo and have it still be the same entry in the user's library" is the other 80%.&lt;/p&gt;

&lt;p&gt;A naive converter produces a fresh image stripped of context: no album membership, no favorite status, no caption, dropped EXIF and GPS, original creation date replaced by "now." That's worthless for a real library. So a large chunk of the work is metadata and relationship preservation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EXIF / TIFF / GPS / orientation / camera settings&lt;/strong&gt; are carried forward at encode time. The encoder writes the source's properties into the HEIC as it's created, rather than trying to patch them in afterward (more on why "afterward" is a trap below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Album membership&lt;/strong&gt; is restored by re-adding the new asset to each collection the original belonged to — and as noted above, a failure here vetoes the original's deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Favorites, captions, creation date&lt;/strong&gt; are mirrored onto the new asset via &lt;code&gt;PHAssetChangeRequest&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The encode-time-vs-after-the-fact distinction matters more than I expected, which brings me to the gotchas.&lt;/p&gt;

&lt;h2&gt;
  
  
  ImageIO and PhotoKit will lie to you politely
&lt;/h2&gt;

&lt;p&gt;Several days of this project were spent discovering platform behavior that is technically documented somewhere but bites hard in practice. A few that are worth knowing if you're in this neighborhood:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auxiliary data (gain maps) silently drops on finalize if the dictionary is incomplete.&lt;/strong&gt; &lt;code&gt;CGImageDestinationAddAuxiliaryDataInfo&lt;/code&gt; will happily accept your aux data and then &lt;em&gt;drop it on &lt;code&gt;CGImageDestinationFinalize&lt;/code&gt;&lt;/em&gt; if the dictionary is missing &lt;code&gt;kCGImageAuxiliaryDataInfoDataDescription&lt;/code&gt; (width, height, pixel format, bytes-per-row). No error. The file just finalizes without the gain map. For ISO 21496-1 aux specifically, you also need the &lt;code&gt;Metadata&lt;/code&gt; entry, or the aux writes empty. And the &lt;code&gt;Metadata&lt;/code&gt; field is a &lt;code&gt;CGImageMetadata&lt;/code&gt; ref, not &lt;code&gt;Data&lt;/code&gt; — if you try to round-trip it with a naive &lt;code&gt;as? Data&lt;/code&gt; cast it silently fails, and you have to go through &lt;code&gt;CGImageMetadataCreateXMPData&lt;/code&gt; / &lt;code&gt;CGImageMetadataCreateFromXMPData&lt;/code&gt;. None of these failures announce themselves. You find them by opening the output and noticing the HDR is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-encoding a decoded gain-mapped HEIC primary fails &lt;code&gt;Finalize&lt;/code&gt; on iOS 26.&lt;/strong&gt; If you decode a gain-mapped HEIC and try to write it back out through any &lt;code&gt;CGImageDestination&lt;/code&gt; path to patch metadata after the fact, finalize fails. The lossless &lt;code&gt;CGImageDestinationCopyImageSource&lt;/code&gt; works, but its XMP merge can't populate the classic EXIF/TIFF property dictionaries. The practical consequence drove an architecture decision: &lt;strong&gt;write metadata at original encode time, not as a post-pass.&lt;/strong&gt; If you're planning a "convert now, fix metadata later" pipeline, test that assumption early, because on current OSes the later step may not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some metadata just doesn't survive HEIC, period.&lt;/strong&gt; Canon's MakerNote, for instance, doesn't make it through HEIC encoding — that's an ImageIO/HEIC platform limitation, not something I can fix. I keep a regression test that asserts the loss so I notice if the platform ever changes, but I'm honest in the app's design that it's a known boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auxiliary metadata doesn't round-trip via the public API.&lt;/strong&gt; &lt;code&gt;kCGImageAuxiliaryDataInfoMetadata&lt;/code&gt; does not come back via &lt;code&gt;CGImageSourceCopyAuxiliaryDataInfoAtIndex&lt;/code&gt; on macOS 15 / iOS 18. The &lt;code&gt;apple:HDRGainMapHeadroom&lt;/code&gt; XMP tag gets written (Photos itself presumably reads it via private API) but you can't read it back publicly to assert on it. The lesson for testing: assert on &lt;em&gt;observable&lt;/em&gt; contracts, not on values the public API won't return to you.&lt;/p&gt;

&lt;p&gt;The meta-lesson across all of these: ImageIO's failure mode is silence. It returns success and gives you a subtly wrong file. The only defense is to read your own output back and verify properties, which — usefully — is the same discipline the delete-safety invariants already demanded.&lt;/p&gt;

&lt;h2&gt;
  
  
  HDR is where it got genuinely interesting
&lt;/h2&gt;

&lt;p&gt;Modern photos carry HDR via a gain map — a secondary image that tells a display how to brighten highlights beyond SDR. Preserving or synthesizing that correctly is most of what separates a serious converter from a toy.&lt;/p&gt;

&lt;p&gt;The policy I landed on has three branches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;iPhone ProRAW DNG:&lt;/strong&gt; render an HDR companion via &lt;code&gt;CIRAWFilter&lt;/code&gt;'s &lt;code&gt;extendedDynamicRangeAmount&lt;/code&gt;, then let ImageIO compute the calibrated auxiliary gain map from the &lt;code&gt;(SDR, HDR)&lt;/code&gt; pair. I tried extracting the embedded aux from the ProRAW DNG directly and removed it — it required &lt;code&gt;boostShadowAmount = 0&lt;/code&gt; to match Apple's private SDR base, which made the file render dark in non-HDR-aware viewers like Quick Look and browsers. Letting ImageIO derive the aux from a proper SDR/HDR pair produces a file that looks right &lt;em&gt;everywhere&lt;/em&gt;, which matters more than matching one private code path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other RAW (Canon, Sony, Nikon, Fuji…):&lt;/strong&gt; synthesize an ISO 21496-1 gain map from the RAW's extended dynamic range, where the sensor data actually supports it. Don't fabricate HDR that wasn't captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edited-in-Photos inputs:&lt;/strong&gt; flat SDR. If someone already baked their look in Photos, inventing an HDR interpretation on top is the wrong call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The principle underneath all three: match what the source genuinely contains, and produce a file that degrades gracefully on viewers that don't understand gain maps. HDR that only looks right in one app isn't preserved, it's trapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a long batch feel calm
&lt;/h2&gt;

&lt;p&gt;Converting five hundred RAWs is not instant, so the UX problem is "don't make the user stare at a spinner or babysit the app."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Activity&lt;/strong&gt; via ActivityKit shows progress on the Lock Screen and Dynamic Island on iPhone/iPad, and a menu-bar tile on Mac. Content updates are rate-limited to ≤ 1 Hz with a coalescing limiter — ActivityKit punishes chatty updates, and nobody needs 60 fps progress text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS 26's &lt;code&gt;BGContinuedProcessingTask&lt;/code&gt;&lt;/strong&gt; lets large batches keep running with the screen off, which is the difference between "convert your whole library" being realistic or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On iOS/iPadOS, conversion is a serial async loop&lt;/strong&gt; — one item at a time, deliberately, to stay well within memory limits. &lt;strong&gt;On macOS there's an opt-in Turbo Mode&lt;/strong&gt; that runs bounded-concurrent (a heuristic on cores and memory, clamped to 2–6 and never exceeding cores − 2). The single batched &lt;code&gt;deleteAssets&lt;/code&gt; still runs exactly once after everything drains. The heavy per-item render runs off the actor; the bookkeeping — DB writes, progress, outcome recording — stays serialized on it. PhotoKit writes are funneled through a single &lt;code&gt;PhotoLibrary&lt;/code&gt; actor so they're always ordered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a SQLite-backed queue underneath so a batch is durable and resumable, and database migrations run exactly once at launch before any data access — boring infrastructure that earns its keep the first time someone force-quits mid-batch and reopens to find their place held.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another developer
&lt;/h2&gt;

&lt;p&gt;Three things, if you're building in this space:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide App Intent vs. Share Extension by the permissions you need, not by familiarity.&lt;/strong&gt; If you need to mutate or delete library assets, the App Intent path is the one that actually works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your irreversible-action safety rules as explicit, testable invariants before the feature exists.&lt;/strong&gt; "Verify before delete, batch the deletes, make the off-path unreachable" reads like common sense and is shockingly easy to violate by accident under refactoring. Tests are how you keep it true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust nothing ImageIO returns; read your output back.&lt;/strong&gt; Its failure mode is a confident success with a quietly wrong file. Verification isn't paranoia here, it's the contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see the result of all this, RawToHEIC is on the &lt;strong&gt;&lt;a href="https://apps.apple.com/app/id6763160757" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;&lt;/strong&gt; — a one-time $19.99, universal across iPhone, iPad, and Mac, fully on-device, zero telemetry. There's more on the formats and the HDR handling at &lt;strong&gt;&lt;a href="https://rawtoheic.app" rel="noopener noreferrer"&gt;rawtoheic.app&lt;/a&gt;&lt;/strong&gt;. And if you're a developer hitting any of these same ImageIO or PhotoKit walls, my inbox is open — I learned most of this the slow way and I'm happy to compare notes.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>programming</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Stop Over-Engineering: A 100-line bash script that saved my servers</title>
      <dc:creator>Sandro 🦖☄️</dc:creator>
      <pubDate>Mon, 20 Oct 2025 05:20:04 +0000</pubDate>
      <link>https://dev.to/sgumz/stop-over-engineering-a-100-line-bash-script-that-saved-my-servers-134g</link>
      <guid>https://dev.to/sgumz/stop-over-engineering-a-100-line-bash-script-that-saved-my-servers-134g</guid>
      <description>&lt;p&gt;We've all been there. Your website goes down at 3 AM. MySQL crashed. NGINX stopped responding. And you're scrambling to SSH into the server while your phone buzzes with angry customer emails.&lt;/p&gt;

&lt;p&gt;Then someone suggests: &lt;em&gt;"You should use Prometheus + Grafana + Alertmanager + PagerDuty!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sure. Or... hear me out... you could just use a &lt;strong&gt;100-line bash script&lt;/strong&gt; that checks your sites every minute and restarts services automatically when they fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Enterprise Monitoring
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong - tools like Datadog, New Relic, and Prometheus are amazing. But they're also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;strong&gt;Overkill&lt;/strong&gt; for small projects&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Expensive&lt;/strong&gt; for startups&lt;/li&gt;
&lt;li&gt;🧩 &lt;strong&gt;Complex&lt;/strong&gt; to set up and maintain&lt;/li&gt;
&lt;li&gt;🐌 &lt;strong&gt;Slow&lt;/strong&gt; to deploy (days/weeks of configuration)&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Require&lt;/strong&gt; learning new query languages and dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, your website is still down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter: The 100-Line Solution
&lt;/h2&gt;

&lt;p&gt;What if monitoring could be this simple?&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. Add your websites&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; sites.txt

&lt;span class="c"&gt;# 2. Install&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./install.sh

&lt;span class="c"&gt;# 3. Done. Seriously.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every minute, your server now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Checks if your websites respond&lt;/li&gt;
&lt;li&gt;🔍 Detects if services are overwhelmed (not just down!)&lt;/li&gt;
&lt;li&gt;🔧 Automatically restarts MySQL, NGINX, or Apache&lt;/li&gt;
&lt;li&gt;📝 Logs only failures (no disk space waste)&lt;/li&gt;
&lt;li&gt;🔄 Tracks failure counts to avoid false positives&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How It Works (The Smart Part)
&lt;/h2&gt;

&lt;p&gt;Most monitoring tools just check if a service is "running." That's not enough.&lt;/p&gt;

&lt;p&gt;Here's what makes this script &lt;strong&gt;intelligent&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Load-Based Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Don't just check if MySQL is running...&lt;/span&gt;
&lt;span class="c"&gt;# Check if it's actually RESPONSIVE&lt;/span&gt;
check_mysql_health&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Try to ping MySQL&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;timeout &lt;/span&gt;3 mysqladmin ping&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="c"&gt;# It's alive! But is it overwhelmed?&lt;/span&gt;
        &lt;span class="nv"&gt;current_connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mysqladmin status | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'Threads: \K\d+'&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="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$current_connections&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 150 &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="c"&gt;# Too many connections - restart before it crashes&lt;/span&gt;
            &lt;span class="k"&gt;return &lt;/span&gt;1
        &lt;span class="k"&gt;fi
    fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your site can be down even when services show as "running" - when they're overloaded with traffic or locked up processing queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Advanced Health Checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# NGINX example: Test config + connectivity + load&lt;/span&gt;
check_nginx_health&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# 1. Validate config before trying to use it&lt;/span&gt;
    nginx &lt;span class="nt"&gt;-t&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1

    &lt;span class="c"&gt;# 2. Can it accept connections?&lt;/span&gt;
    &lt;span class="nb"&gt;timeout &lt;/span&gt;2 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo &amp;gt; /dev/tcp/localhost/80"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1

    &lt;span class="c"&gt;# 3. Is it drowning in connections?&lt;/span&gt;
    &lt;span class="nv"&gt;active_conn&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; http://localhost/nginx_status | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'Active connections: \K\d+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$active_conn&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 1000 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1

    &lt;span class="k"&gt;return &lt;/span&gt;0  &lt;span class="c"&gt;# All good!&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Smart Recovery Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only restart after 3 consecutive failures (avoid false positives)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$current_failures&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 3 &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="c"&gt;# Restart services in order: Database first, then web server&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;service &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&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;do
        &lt;/span&gt;systemctl restart &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;done
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;Let's say your e-commerce site suddenly gets featured on Reddit (congrats! 🎉). Traffic spikes 10x:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional Monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📊 Dashboards show high CPU/memory&lt;/li&gt;
&lt;li&gt;🚨 Alerts fire&lt;/li&gt;
&lt;li&gt;👨‍💻 You get paged&lt;/li&gt;
&lt;li&gt;⏰ You wake up, investigate, manually restart services&lt;/li&gt;
&lt;li&gt;💸 Lost sales during downtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This Script:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 Detects MySQL has 200 active connections (threshold: 150)&lt;/li&gt;
&lt;li&gt;🤖 Automatically restarts MySQL in 3 seconds&lt;/li&gt;
&lt;li&gt;📝 Logs: &lt;code&gt;"MySQL OVERLOADED (200 connections) - restarted"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;😴 You stay asleep&lt;/li&gt;
&lt;li&gt;💰 Sales continue&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation (Seriously, It's This Easy)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone the repo&lt;/span&gt;
git clone https://github.com/YOUR_USERNAME/site-monitor.git
&lt;span class="nb"&gt;cd &lt;/span&gt;site-monitor

&lt;span class="c"&gt;# 2. Add your websites&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sites.txt &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
https://example.com
https://api.example.com
https://www.example.com
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# 3. Optional: Customize thresholds&lt;/span&gt;
vim config.conf  &lt;span class="c"&gt;# Adjust MySQL/NGINX/Apache thresholds&lt;/span&gt;

&lt;span class="c"&gt;# 4. Install (creates cron job, sets up logging)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./install.sh

&lt;span class="c"&gt;# 5. Watch it work&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/site-monitor/monitor.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2025-10-20 14:23:45] FAILURE: https://example.com - HTTP 000 (1/3 failures)
[2025-10-20 14:24:45] FAILURE: https://example.com - HTTP 000 (2/3 failures)
[2025-10-20 14:25:45] FAILURE: https://example.com - HTTP 000 (3/3 failures)
[2025-10-20 14:25:46] RECOVERY: Starting recovery for https://example.com
[2025-10-20 14:25:47] RECOVERY: MySQL OVERLOADED (187 connections) - restarted
[2025-10-20 14:25:49] RECOVERY: NGINX responsive - no action needed
[2025-10-20 14:25:50] RECOVERY: Recovery completed
[2025-10-20 14:26:45] SUCCESS: https://example.com back online (HTTP 200)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration Options
&lt;/h2&gt;

&lt;p&gt;Everything is configurable in &lt;code&gt;config.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# HTTP Settings&lt;/span&gt;
&lt;span class="nv"&gt;TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10                    &lt;span class="c"&gt;# Request timeout&lt;/span&gt;
&lt;span class="nv"&gt;FAILURE_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3           &lt;span class="c"&gt;# Failures before recovery&lt;/span&gt;

&lt;span class="c"&gt;# Services to manage (in order)&lt;/span&gt;
&lt;span class="nv"&gt;SERVICES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"mysql"&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;    &lt;span class="c"&gt;# Or: ("mysql" "apache2")&lt;/span&gt;

&lt;span class="c"&gt;# Load Thresholds&lt;/span&gt;
&lt;span class="nv"&gt;MYSQL_MAX_CONNECTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;150     &lt;span class="c"&gt;# Restart if connections exceed this&lt;/span&gt;
&lt;span class="nv"&gt;NGINX_MAX_CONNECTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000    &lt;span class="c"&gt;# Restart if connections exceed this&lt;/span&gt;
&lt;span class="nv"&gt;APACHE_MAX_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;150        &lt;span class="c"&gt;# Restart if busy workers exceed this&lt;/span&gt;

&lt;span class="c"&gt;# Logging&lt;/span&gt;
&lt;span class="nv"&gt;LOG_SUCCESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;             &lt;span class="c"&gt;# Only log failures (save disk space)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Use This vs. Enterprise Tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use This Simple Script When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 You have &amp;lt; 50 websites to monitor&lt;/li&gt;
&lt;li&gt;💰 You're on a budget (it's free!)&lt;/li&gt;
&lt;li&gt;⚡ You need it deployed TODAY&lt;/li&gt;
&lt;li&gt;🔧 You manage your own Ubuntu servers&lt;/li&gt;
&lt;li&gt;🎓 You want to understand what's happening (no black box)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Enterprise Tools When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📊 You need fancy dashboards and metrics&lt;/li&gt;
&lt;li&gt;🌍 You have distributed microservices&lt;/li&gt;
&lt;li&gt;👥 You have a dedicated DevOps team&lt;/li&gt;
&lt;li&gt;💼 You need compliance/audit trails&lt;/li&gt;
&lt;li&gt;🔗 You need integration with 50+ other tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance &amp;amp; Resource Usage
&lt;/h2&gt;

&lt;p&gt;This script is incredibly lightweight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt;: Near zero (runs for ~1 second per minute)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: ~5MB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk&lt;/strong&gt;: &amp;lt;1MB logs per month (with default settings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt;: One HTTP GET per site per minute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare that to running Prometheus + Grafana (hundreds of MB of RAM).&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-Ready Features
&lt;/h2&gt;

&lt;p&gt;Don't let the simplicity fool you - this runs in production:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;State Tracking&lt;/strong&gt;: Counts consecutive failures per site&lt;br&gt;
✅ &lt;strong&gt;Log Rotation&lt;/strong&gt;: Yearly rotation via logrotate&lt;br&gt;
✅ &lt;strong&gt;Error Handling&lt;/strong&gt;: Graceful failures, timeout protection&lt;br&gt;
✅ &lt;strong&gt;No Dependencies&lt;/strong&gt;: Just bash + curl + systemctl (already on Ubuntu)&lt;br&gt;
✅ &lt;strong&gt;Tested&lt;/strong&gt;: Works on Ubuntu 22.04 LTS&lt;/p&gt;
&lt;h2&gt;
  
  
  Advanced Use Cases
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Multi-Server Deployment
&lt;/h3&gt;

&lt;p&gt;Deploy to multiple servers with different site lists:&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;# Server 1: Monitor frontend sites&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://app.example.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sites.txt

&lt;span class="c"&gt;# Server 2: Monitor API endpoints&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sites.txt

&lt;span class="c"&gt;# Server 3: Monitor admin tools&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://admin.example.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sites.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Services
&lt;/h3&gt;

&lt;p&gt;Not just MySQL/NGINX! Add any systemd service:&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;# Add Redis, PHP-FPM, whatever you need&lt;/span&gt;
&lt;span class="nv"&gt;SERVICES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"mysql"&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt; &lt;span class="s2"&gt;"redis-server"&lt;/span&gt; &lt;span class="s2"&gt;"php8.1-fpm"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration with Existing Tools
&lt;/h3&gt;

&lt;p&gt;Still want Slack notifications? Just add a webhook:&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;# In monitor.sh, add after line 320:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"YOUR_SLACK_WEBHOOK"&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;text&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;🚨 &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt; is down! Auto-recovering...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Philosophy: Simple &amp;gt; Complex
&lt;/h2&gt;

&lt;p&gt;This project follows the Unix philosophy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do one thing well&lt;/li&gt;
&lt;li&gt;Use plain text for data&lt;/li&gt;
&lt;li&gt;Build small, composable tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your monitoring doesn't need to be fancy. It needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect failures&lt;/strong&gt; ✅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix them automatically&lt;/strong&gt; ✅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell you what happened&lt;/strong&gt; ✅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mission accomplished in 100 lines of bash.&lt;/p&gt;

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

&lt;p&gt;The code is open source (MIT License):&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/sgumz/site-monitor" rel="noopener noreferrer"&gt;https://github.com/sgumz/site-monitor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installation takes 2 minutes. Give it a try!&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Sometimes the best solution isn't the one with the most features - it's the one that &lt;strong&gt;solves your problem today&lt;/strong&gt; without creating new ones.&lt;/p&gt;

&lt;p&gt;Could this bash script replace Datadog for a Fortune 500 company? No.&lt;/p&gt;

&lt;p&gt;Could it save your small SaaS business from 3 AM wake-up calls? Absolutely.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your take?&lt;/strong&gt; Do you prefer simple scripts or enterprise monitoring? Any horror stories about over-engineered solutions? Drop a comment below! 👇&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
