Tag: Automation

  • On This Day: Twenty-Six Years of Photos Back in My Notes

    I’ve been taking photos and putting them on Flickr for a very long time, and like most people I almost never look back at them. Thousands of pictures from holidays, days out and events, all sitting in albums that I last opened the day I uploaded them.

    Meanwhile my Obsidian vault has a note for every trip and a lot of the events I’ve been to, and a Daily note that I open every morning. The two never met. So last weekend I asked Claude Code to connect them, and the result is a small project called FlickrVault. It does two things: it puts an “On This Day” section with old photos into every Daily note, and it links my Flickr albums to the matching Trips and Events notes. (For the wider picture of what runs on my Raspberry Pi, see What I Have Built With Claude Code (So Far).)

    On This Day

    Every night, shortly after the n8n workflow that creates my Daily note has run, a script adds a section headed “On This Day”. For each earlier year in which I took photos on today’s date, it shows:

    • The year and how long ago it was, linked to that day’s photos on Flickr.
    • How many photos I took that day.
    • Up to three thumbnails, spread through the day rather than the first three, so a day that started at breakfast and ended at a show gets a picture from each part. Each thumbnail links to the photo on Flickr.
    • Any Trip or Event note that covers that day, so I can jump straight from “26 years ago” to my notes about where I was.

    It does one Flickr search per year, starting from 1995, which comes to about 30 calls and takes around 45 seconds. Yesterday’s section went all the way back to 2000, 26 years ago. Private photos appear too, which is fine because the vault is private: the thumbnails load without signing in, and the links to Flickr need me to be signed in.

    A few details make it reliable rather than just working on a good day:

    • It never creates the Daily note itself. If n8n hasn’t made it yet, the script logs that it’s waiting and a second run at 06:15 tries again.
    • Re-runs are harmless. If the section is already there, nothing happens. The note is re-read just before writing and replaced in one go, so it can’t trample on something I’ve typed in the meantime.
    • It doesn’t use AI. This is plain Python with no model in the loop. Picking photos by date doesn’t need judgement, so there’s nothing to go wrong and nothing to pay for.

    Flickr’s API isn’t always cooperative. The run log has a fair number of “the Flickr API service is not currently available” errors, but the scheduled runs have filled in every Daily note since it went live on 24 October.

    Linking albums to trips

    The second job runs every Sunday morning and works out which Flickr albums belong to which Trips and Events notes. Each of those notes already has start and end dates, so the rule is simple: an album matches a note when at least 80% of its photos were taken within the note’s dates. What happens next depends on the album’s name:

    • The name fits as well. If the album name shares a word with the note’s name or location, the album’s link goes straight into a flickr_album: list in the note’s properties. The word match is fuzzy, so a misspelt album name like “Maderia” still matches “Madeira”.
    • Only the dates fit. A birthday album taken during a trip, for example, matches on dates but probably isn’t part of the trip. These go into a review note as tasks instead.
    • The name and year fit, but only some of the photos do. If 30–80% of the photos are inside the dates, that also goes to review.

    In the review note I tick a task to link the album, or mark it as cancelled to reject it, and the next Sunday run applies my choice. Rejected pairs are never suggested again, and links that are already in a note are never removed or reordered.

    The first run took a while, because it had to fetch the photo dates for every album (around 800 Flickr calls). Those dates are cached, and only albums that are new or have changed are fetched again, so the weekly run is quick.

    Panorama of a harbour with a row of brightly painted buildings along the waterfront and boats moored in the bay
    Tobermory, from my 2020 trip to Mull. Both of that trip’s albums are now linked from its note.

    What the dry run caught

    Before letting it change any notes, Claude ran the matching as a dry run first. Three problems showed up, all down to the way I’ve named albums over the years:

    • Squashed-together names. Albums named like MNDBall-2015 or Holiday2024-Orkney weren’t being split into words, so they never matched anything by name. The matcher now splits on changes of case and on digits, so those become “mnd ball” and “orkney”. Words like “holiday” and “trip” are ignored, because they’d match everything.
    • parkrun the day before. I have plenty of parkrun albums. Because a match on dates alone allowed a day either side, a Saturday parkrun album was being suggested for a one-day event on the Sunday. Now a match on dates alone has to be strictly inside the note’s dates; the extra day either side is kept only when the name fits too, to allow for photos from the journey and cameras with the wrong clock.
    • Catch-all albums. Favourites, auto-upload and year-only albums were turning up as suggestions. They’re never about one trip or event, so they’re now ignored completely.

    After those fixes, the first real run linked 50 albums to 40 notes (34 trips and 6 events) and put 39 less certain matches in the review note. Some of those are clearly right (an album of a space centre visit during a Florida holiday has a name that shares no words with the trip). I’ll admit that I haven’t worked through them yet: all 39 are still waiting for me.

    Two weathered, abandoned wooden fishing boats lying on a stony shore beside a calm loch, with hills behind
    Old boats at Salen, Mull, 2020.

    Keeping track of which photos are used

    Once photos started appearing in my notes (and, as it happens, in posts on this blog), I wanted to be able to see on Flickr which ones were being used. So every photo that something on the Pi uses gets two tags, claudelinked and one saying what it was used for, and is added to a matching “Linked” album. The On This Day job marks the thumbnails it shows, and the Sunday job finishes by scanning the whole vault for links to my photos, albums and days and marking all of them.

    That scan taught a small lesson about what counts as a failure. On its first big run a handful of Flickr calls failed with gateway timeouts, the run retried them and sent them all, and the run still logged itself as failed, which set off my regression tests. It now counts as failed only if photos are still waiting to be sent when it finishes.

    How it fits with everything else

    • One shared Flickr client. FlickrVault doesn’t talk to Flickr directly. It uses the same small client as everything else on the Pi, which can only read. The one exception is marking used photos, which goes through a separate tool.
    • EventCapture adds photos as it goes. My weekly EventCapture run, which records the shows, meals and days out it finds in my email and calendar, now also checks Flickr for photos from each event and adds an album, or the day’s photos once it has looked at a few to check they’re from the event. The Sunday album job catches anything I upload later.
    • The usual rules. Its own git repo and README, a changelog note in the vault, and a regression test that checks the timers are running, today’s section is there, and the last album run didn’t fail.
    A white lighthouse on a low island under heavy grey clouds tinged with pink at dusk
    A lighthouse at dusk on the last evening of the Mull trip.

    Was it worth it?

    It’s only been running for a week, but yes. Three small photos from a day 16 or 18 years ago, at the top of the note I open every morning, are a far better way back into my photos than albums I never open. The trip links matter more in the long run: my notes about a holiday and the photos from it are finally one click apart.

    Now I just need to work through those 39 suggestions.

    A note on how this post was written: it was drafted by Claude Code from the FlickrVault repo, its code history and my vault notes, and Claude chose the photos from my Flickr account. It wrote the draft directly into WordPress through the WordPress API, and I then reviewed and edited it before publishing.

  • My Home Server Has Found Its Voice

    My Home Server Has Found Its Voice

    Until this week, everything my Raspberry Pi did was silent. It wrote notes, filled in dashboards, sent the odd email and scrolled messages across a little LED panel on my desk, but if I wasn’t looking at a screen I didn’t know about any of it. Now it can talk.

    On 29 October Claude Code and I built two small things: a command-line tool that plays speech or audio on the Google speakers around the house, and a Voice switch on my LED display’s control page that reads out whatever the display is showing. The speech itself is generated on the Pi, not by a cloud service. This post is how it fits together and the few things that tripped us up.

    Why bother?

    I already had the speakers, and I already had a lot of automations producing small bits of useful information: the next train, which bin goes out, what’s flying overhead. Reading them means looking at something. Hearing them doesn’t.

    I started it the way I start most things now: I dropped a task note into my vault’s Inbox asking for a way to play audio on the Google speakers. Before anything was built, I made three choices:

    • Both speech and audio files, not just one or the other.
    • The office speaker as the default, since that’s where I am when I’m working on this stuff.
    • Local text-to-speech rather than the Google Translate voice that Home Assistant already had set up. I’d rather the words my scripts generate weren’t sent off to be turned into audio somewhere else.

    The pieces

    Most of the work turned out to be already done. Home Assistant was running and already knew about every speaker in the house, so the job was mainly joining things up:

    • Home Assistant’s Google Cast integration exposes each speaker, and each speaker group, as a media player that can be told to play something.
    • Piper, an open-source text-to-speech engine, runs in its own Docker container on the Pi. It uses the British English en_GB-alba voice.
    • The Wyoming integration connects Home Assistant to Piper, which gives Home Assistant a new text-to-speech engine to use instead of the cloud one.
    • Home Assistant’s REST API is the single way in. Anything on the Pi that wants to speak makes one API call that says “say this, on that speaker”, and Home Assistant gets Piper to render the audio and tells the speaker where to fetch it.

    The access token for that API lives in a locked-down file on the Pi, like every other secret in this setup, and is read at run time.

    SpeakerCast: one command

    The first piece is SpeakerCast, a bash script of about a hundred lines wrapped around that API. It does three things:

    • speakercast say "…" speaks some text, on the office speaker unless I name another speaker or group.
    • speakercast play plays an audio file or a URL. A local file is copied into a folder Home Assistant serves on my home network, so the speaker can fetch it, and anything in that folder older than a week is tidied away on each run.
    • speakercast list shows every speaker and group Home Assistant knows about.

    There’s an option to set the volume first, and a dry-run option that prints the calls it would make without making a sound. That last one matters more than it sounds: like every service on the Pi, SpeakerCast has a regression test, and a test that runs regularly and says “test” out loud in my office would not last long. The test checks everything short of actually playing anything.

    It was built after midnight, so the only thing we couldn’t check that night was the bit you can hear. I ran the audible test the next morning, a “hello” and a local MP3 on the office speaker, and both played.

    The one gotcha

    Playing a local file needs Home Assistant to serve it over its /local/ web path, which comes from a www folder in its configuration. The catch is that Home Assistant only notices that folder if it exists when Home Assistant starts. Creating it isn’t enough; it needed one restart. Because a restart takes down the things that depend on Home Assistant, Claude waited until after my daily note had been created for the next day before doing it.

    Giving the LED display a voice

    The more fun half came that evening. On my desk there’s a Pi Zero with a Pimoroni Unicorn HAT HD, a 16×16 LED matrix that scrolls messages, with a web control page that’s part of my RailData server. It has a set of quick-send buttons that each fetch something and scroll it across the panel.

    That page now has a Voice Off/On switch. With it on, anything I send to the panel is also spoken on the office speaker:

    • any message I type in
    • the next trains
    • the next bin collection
    • the aircraft overhead (the feed behind it was the subject of last week’s post)
    • the AuroraWatch status
    • command words like “time” and “date”

    The switch starts Off, and the browser remembers it. Automated messages and the background animations stay silent, so the speaker only talks when I’ve asked it to.

    It uses exactly the same route as SpeakerCast: one Home Assistant call with the Piper engine. If the speaker call fails, the error shows on the page, but the message still goes to the panel. The display was there first, so speech is never allowed to break it.

    Writing for the ear, not the eye

    Text written for a 16-pixel-high panel is full of shorthand, and Piper reads exactly what it’s given. So the server rewrites each message before it’s spoken:

    • Command words become sentences. On the panel, “datetime” is drawn by the Pi Zero itself, so the server has to work out what to say, something like “It’s 20:17 on Thursday 29 October.”
    • Shorthand is expanded. The dots that separate items become pauses, arrows become “to”, “ft” and “mi” become feet and miles, “exp” becomes “expected”, and compass points like NE become “north east”.
    • Several messages become one sentence. Trains and bins send more than one message at a time. Speaking each one as a separate call doesn’t work, because the second call cuts the first off mid-sentence, so they’re joined into a single utterance.

    That last one is easy to miss. On a screen, two messages one after the other are fine; out loud, one stamps on the other.

    A pomodoro timer that tells you when to stop

    The display also has a pomodoro timer mode, and later the same evening it got a voice too. With Voice on, I hear “Pomodoro started: 25 minutes” at the start, the time left when I pause or resume, “Pomodoro stopped” if I give up, and at the end “Pomodoro complete. Your 25 minutes are up, time for a break.”

    This one needed a bit more thought, because the timer runs on the Pi Zero, not on the server. If the server tried to work out when 25 minutes were up, it would get it wrong as soon as I paused. So the Pi Zero now publishes an event over MQTT each time the timer changes state, and the server, which was already listening to the display for its live preview, picks up those events and speaks them. The finish is timed by the display itself, so it’s right after a pause, and it’s announced even if I’ve closed the control page.

    We tested it with a one-minute timer: the start and the finish both played on the office speaker.

    What I’ve learned

    • Check what you already have. Home Assistant already knew every speaker in the house. The whole thing was a container, one integration and a hundred lines of bash.
    • Keep the speech local. Piper runs in a small container next to everything else, and the text my scripts generate isn’t sent to a cloud voice service to be turned into audio.
    • Make it opt-in and quiet by default. The switch starts Off, the tests never make a sound, and nothing automated speaks unless I’ve turned it on. A house that talks without being asked would get switched off quickly.
    • Design for the ear. Shorthand, symbols and back-to-back messages all work on a screen and all fail out loud.

    It also gives Claude a new way to reach me: I’ve told it that when an interactive session needs me, it can ask to announce that on the office speaker rather than waiting for me to notice the terminal.

    A note on how this post was written: it was drafted by Claude Code from the SpeakerCast and RailData repos, their code history and the notes in my Obsidian vault, and written into WordPress as a draft through the WordPress API. The header image was generated with Google’s Gemini image model. I then reviewed and edited it before publishing.

  • The Watchdog’s First Real Case: A Slow Aircraft Feed

    Last weekend I wrote about the watchdog that now writes its own investigation tasks when a regression test on the Pi starts failing, so that Claude Code picks them up without me being the messenger. I finished that post by saying nothing had failed since it went in, so no real investigation task had been raised yet.

    That turned out not to be true. The first real one had been raised just before midnight the night before the post went out. I just hadn’t noticed it yet. This post is about that task: what broke, what Claude found, why it stopped and asked me rather than fixing it, and what the fix ended up being.

    The feature that broke

    The failing check belonged to one of the sillier things I’ve built. The Pi Zero in the house with a Unicorn HAT HD LED matrix has a small web control page, and in September I added an “Aircraft overhead” button to it. Press it, and the panel scrolls the closest plane currently in the sky near home, something like:

    • EAI55SK · AT76 · EDI→DUB · 8200ft · 8.5mi ESE
    Two aircraft contrails crossing in a blue sky to form a saltire
    Not every plane overhead needs an LED panel to announce it: two contrails making a saltire over Scotland.

    That’s the callsign, the aircraft type, the route, the altitude, and the distance and compass bearing from home. It was built in a few steps:

    • The aircraft data comes from free community ADS-B feeds, run by volunteers with receivers. adsb.lol was the main feed, with adsb.fi as the fallback if it errored. (My first choice, airplanes.live, now refuses requests unless you register.)
    • The radius started at 5 miles, which mostly produced “No aircraft within 5 mi”, so a day later it went up to 10.
    • The route isn’t part of the ADS-B signal at all, only the callsign is. So the route is looked up separately against adsbdb.com, a free community database of airline schedules, and simply left out if there’s no match.

    It lives in RailData, the Flask app on the Pi that has become home to most of my small odds and ends, and like everything else it has regression tests. One of them simply asks the aircraft endpoint for an answer and expects to get one within the suite’s 20-second limit.

    Saturday night, 23:20

    Here’s how it played out, using the timestamps in the task note and the logs:

    • 23:20. The aircraft check failed for the first time. The test got an empty response instead of an answer.
    • 23:34. It failed again on the next run. Two failures in a row is the rule for a failure counting as real, so the watchdog wrote an investigation task into the vault’s Inbox folder.
    • Just before midnight. InboxProcessor picked the task up, Claude Code investigated, wrote up its findings, and marked the task as blocked, waiting for me.
    • Sunday evening. I read it, wrote down my decision, and the fix went in shortly after 20:20.

    Nothing was emailing me every 15 minutes in the meantime, which was exactly the point of last week’s changes.

    What Claude found

    The investigation was good, and I mean that in the sense that it went past the symptom. The service itself was healthy: running, no errors, every other check passing. The problem was upstream.

    • The main feed had become very slow. Timing adsb.lol directly from the Pi gave 11 seconds, then a time-out at 20 seconds, then a time-out at 40 seconds with nothing back at all, then 23 seconds, then 4. Over the same period adsb.fi answered in under a fifth of a second.
    • The fallback worked, but too late. Each request waited out adsb.lol’s 8-second timeout before trying adsb.fi, and then the route lookup ran on top of that.
    • The timeouts didn’t mean what they looked like. This was the subtle bit. Python’s urlopen timeout applies to each socket operation, not to the whole request, so a server that sends data very slowly can keep a request going well past the number in the code. Add up the feed, the fallback and the route lookup, and the worst case was over 20 seconds.
    • Only the first request was slow. The logs showed the first aircraft request in each failing run taking around 23–25 seconds, and the ones after it finishing almost together, because by then the cache had been filled.

    Re-running the RailData tests on their own, everything passed. Claude pointed out that this was only because adsb.lol happened to fail quickly that time, and that it would probably keep flapping for as long as the feed stayed slow.

    Was the test wrong?

    The task tells Claude to consider whether the test is the problem, because a few failures over the summer were false positives. Its answer here was “not really”. A 20-second limit isn’t unreasonable: anyone pressing that button on the control page would also have been waiting 20 seconds or more. The test was reporting a real problem, it just happened to be one caused by somebody else’s server.

    That mattered, because the easiest way to make the failure go away was to give that one check a longer timeout. Claude listed that as an option, and recommended against doing it on its own because it would hide real slowness. That’s the rule I care about most, never weaken a test to hide a real fault, being followed when it would have been the quick way out.

    Why it stopped and asked

    The watchdog’s task defines a simple fix quite narrowly, and one of the things that rules a fix out is “more than one plausible fix”. This had four:

    • Swap the feeds, so the fast one goes first. A one-line change, but it moves the load and the dependency onto adsb.fi.
    • Cap the total time, with a shorter timeout for the main feed and an overall deadline, or query both feeds at once and take the first good answer.
    • Skip the route lookup when time is short, so it can’t add its few seconds on top of a slow fallback.
    • Only relax the test, which it didn’t recommend.

    It suggested the first two together, changed nothing, set the task to blocked and left it in the Inbox with a Findings section and a Blocked section. That is precisely the behaviour I’d asked for, and it’s also what I’d have wanted from a person: here’s the evidence, here are the options, here’s what I’d do, your call.

    My part was one line. I added a Decision heading to the task note saying “Implement option1 and 2”, and put it back in the queue for InboxProcessor to pick up.

    A curving aircraft contrail climbing above autumn trees, with the moon low in the sky
    A plane and the moon over the trees. The feeds that track planes like this one are run by volunteers.

    The fix

    • adsb.fi is now the main feed, with a 4-second timeout, and adsb.lol is the fallback with 8 seconds.
    • The whole lookup has a hard deadline of 10 seconds by default, covering both feeds and the route lookup together. Because urlopen‘s own timeout can’t be trusted to stop a slow trickle of data, each request now runs in a background thread and the code only waits for it until the deadline. If it isn’t finished by then, it’s abandoned.
    • The route lookup is skipped if there’s less than a second left, and that skip isn’t cached, so the route is tried again next time.

    It went in the same way as any other change: a commit pushed to the RailData repository, the README updated, the service restarted at a time that didn’t clash with any of the scheduled jobs, and a changelog note in the vault with instructions for reversing it.

    The results were what I’d hoped for. A fresh request came back in about 0.35 seconds from adsb.fi. With both feeds deliberately made unreachable, the request gave up after exactly 10.0 seconds and still answered with a sensible error rather than hanging. The full regression suite passed: 43 components passed, none failed, and 3 were skipped. Then I got one email with a subject starting “Fixed:”, and the task was archived.

    What I’ve learned

    • Blocking is a good outcome. I’d half expected the first real case to be a test false positive or a container that needed a restart, something Claude could just fix. Instead it was a genuine judgement call, and the most useful thing it did was stop and lay out the options rather than pick one for me.
    • The investigation is the valuable part. I’d never have timed the feeds at midnight. By the time I looked on Sunday the root cause, the evidence and a recommendation were all written down in the task, and my contribution took seconds.
    • Free services can change under you. Nothing in my code changed to cause this. A volunteer-run feed got slow, which is entirely fair for something I don’t pay for, and the right response was to stop depending on it being fast.
    • A timeout isn’t a deadline. I’d have assumed an 8-second timeout meant a request couldn’t take longer than 8 seconds. It can. If something has to answer within a time limit, the limit needs to be enforced around the whole thing.
    • Check before you say “nothing yet”. Last week’s post said no real task had been raised. One had, about thirteen hours before it was published. I’ll take the correction.

    A note on how this post was written: it was drafted by Claude Code, which went through the investigation task note itself, the RailData repository’s code and git history, the InboxProcessor history and the changelog notes in my vault, so that the timeline and numbers here could be checked against what actually happened. Claude also chose the two photos from my Flickr account. It wrote the draft into WordPress as a draft through the WordPress API, and I then reviewed and edited it before publishing.

  • When the Tests Fail, Claude Investigates

    In my last post I walked through InboxProcessor, the job that picks up task notes from my Obsidian vault every 15 minutes and hands them to Claude Code. One of the examples was a failing regression test: the tests emailed me every 15 minutes about Watchtower, I wrote a task from a chat asking Claude to investigate, and it found and fixed the cause while I got on with something else.

    The obvious next question was why I was the one writing that task. This week I took myself out of that step. When a test on the Pi starts failing, the watchdog now writes the investigation task itself, and Claude picks it up.

    How the watchdog got here

    The watchdog has grown in steps, each one prompted by something going wrong:

    • July: run the tests unattended. Paperless crash-looped for about 12 hours after an automatic image update, and nothing told me. The regression suite would have caught it, but nobody was running it. So a systemd timer now runs the whole suite every 15 minutes and emails me if anything fails.
    • September: fix the obvious thing itself. After the NAS was powered down, ten containers sat stopped for about two days because they failed when they were first created, and Docker’s restart policy can’t recover a container that never started. The watchdog spotted it every 15 minutes and emailed me every 15 minutes, but did nothing about it. Now it restarts any container that should be running but isn’t before it runs the tests, and sends a softer warning email to say it did.
    • October: investigate everything else. That’s this post.

    The suite has grown alongside it, from 38 test scripts at the start of the month to 46 now.

    From an email to a task

    Until now a failure produced an email, and the rest was up to me: read it, work out whether it mattered, then either fix it or ask Claude to. In practice, that last step was usually me writing a task note asking Claude to look into it, which is exactly the sort of thing that can be automated.

    So after every run, the watchdog now passes the test output to a small Python script. When a check has started failing, the script writes a task note into the vault’s Inbox folder addressed to Claude Code, and InboxProcessor picks it up on its next run, within 15 minutes. The task note contains:

    • The failing checks, exactly as the test suite reported them, plus when they first failed.
    • Where to look. Re-run just the affected tests, then look at the watchdog’s log, the test script itself, the service’s logs, and the service’s notes in the vault.
    • A reminder that the test might be the problem. Several failures over the summer were false positives, such as the Watchtower race in the last post, so Claude is told to consider that as well as whether the service is actually broken.
    • A decision to make: is this a simple fix, or does it need me?

    Simple fix, or “Action needed”

    The most important part of the task is the definition of a simple fix. It has to be small, low-risk and easily reversible, and within the conventions everything else already follows. It must not need credentials, a login or re-authentication. It must not delete data, cost money or touch anything public-facing. It must not involve a design choice I’d want to make myself. And it must not interrupt any of the scheduled jobs listed in the vault at the time it would be done. The examples given are a test false positive, a crashed container that just needs starting again, or a typo in a config file.

    Everything else needs me, and the task spells out that this includes expired tokens or sessions, anything Claude isn’t confident about, and anything with more than one plausible fix.

    The two outcomes look like this:

    • Simple: fix it properly. The same way as any other change: commit and push to the project’s repository, keep the README current, update the test if needed, run the full suite rather than just the failing test, write a changelog note in the vault, email me with a subject starting “Fixed:”, and archive the task.
    • Not simple: write it up and stop. Add a Findings section (the root cause and the evidence) and a Blocked section (the proposed fix or fixes, and exactly what I need to decide or do), mark the task blocked, leave it in the Inbox, and email me with a subject starting “Action needed:”.

    Either way, I should get exactly one email about it, and the root cause should be written into the task note.

    What it may never do

    A few things are ruled out whatever the investigation finds:

    • Never make a test pass by switching it off or weakening it to hide a real fault. This is the one I care about most. An AI asked to “make the failing test pass” has an easy way out, and taking it would quietly undo the whole point of having tests.
    • Never reboot the Pi, and never take a Docker service down. Stopping a service on purpose is how I tell the auto-heal to leave it alone, so Claude doing it would confuse both of them.
    • Leave the things I’ve deliberately parked alone. A couple of services are broken or stopped on purpose while I decide what to do with them. If one of those turns out to be the cause, the task blocks and says so.

    I should be honest that these are instructions in the task, not hard limits. InboxProcessor runs Claude with full autonomy, as I explained last time, so the boundary between a simple fix and something that needs me is enforced by the prompt rather than by permissions. It’s the same trade-off as every other InboxProcessor task, and the same safety nets apply: the time limit, the lock, and alerts on anything other than a clean finish.

    What counts as “new”

    An investigation every 15 minutes for the same broken thing would be worse than useless, so most of the script is about deciding when a failure is genuinely new:

    • It must fail twice in a row. A check only counts once it has failed on two consecutive runs. That filters out blips, such as the first run after a reboot, when containers are still starting. That case prompted this whole change: after a reboot on Thursday I asked for an Inbox task whenever an error was seen for the first time.
    • Numbers don’t make it new. Each failing check is identified by the component and the check name, with any digits ignored, so a check reporting a changing count doesn’t look like a fresh failure each time.
    • One open task per failure. While a task for that check is still in the Inbox, whether pending, in progress or blocked, nothing new is raised. Once it’s closed, the check only gets another task if it has passed in between and then failed again.
    • One outage, one task. Everything that newly fails in the same run shares a single task, so if the NAS drops and thirty checks fail at once, Claude gets one investigation, not thirty.

    Fewer emails

    Once that logic existed, it was obvious the alert emails should follow it too. Until this week, a failing check sent a critical email on every run, every 15 minutes, for as long as it stayed broken. Now the critical email goes once, with the task, when a failure is confirmed. It lists only the new failures and names the task. Then it stays quiet while the check keeps failing, and the next email I get is Claude’s “Fixed” or “Action needed”.

    There are a few deliberate exceptions. If the test suite itself crashes, there’s nothing to judge, so it always emails. If the vault can’t be reached, the email still goes out, saying the task couldn’t be written, and the task is written on a later run. The “containers were restarted” warning from auto-heal hasn’t changed.

    That’s a trade-off too. A check that fails on and off but never twice in a row now never emails at all. It still shows up in the watchdog’s log and on the regression tests tab of my dashboard, but I’ve accepted that in exchange for not being nagged.

    Testing the thing that watches the tests

    Naturally it has its own regression test. It feeds made-up test output into the script, with a temporary state file and a temporary inbox (never the real vault), and checks the rules: a one-run blip is ignored, a confirmed failure raises exactly one task, repeats are suppressed, a new task is only raised after the check has cleared, the alert goes out once, and an unreachable vault is handled. It started with 10 checks and grew to 15 when the email throttling went in. A task generated by the script was also confirmed to be picked up by InboxProcessor’s real task-finding code, and the full suite passed after each change.

    What I can’t tell you yet is how it does in real life. Nothing has failed since it went in, so no real investigation task has been raised. The end-to-end check is still an unticked box in the changelog, waiting for the next genuine failure. I’m looking forward to it more than I’d like to admit.

    What I’ve learned

    • Look for the step where you’re just the messenger. I was reading an email and then writing a task that said, in effect, “look into this”. Removing that step needed no new service, just one script joining two things that already existed.
    • Write down what “simple” means. “Fix it if it’s easy” is too vague. A specific list (no credentials, no deletion, no design choices, nothing that interrupts a scheduled job) gives Claude something it can actually check against.
    • Deciding when to stay quiet is most of the work. The investigation prompt was quick to write. The rules about what counts as new, and when not to email, took most of the effort and most of the tests.

    A note on how this post was written: it was drafted by Claude Code, which went through the regression test repository (the README, the new script and its git history) and the changelog notes in my vault, so that everything here could be checked against what was actually built. It wrote the draft into WordPress through the WordPress API, and I then reviewed and edited it before publishing.

  • Feedly Mobile App For Android Has Tag Support

    The latest update to the Feedly application for Android has just added support for tags.

    Previously; when I have found something “interesting” in Feedly, I have had to mark it as “Save For Later” and then when I’m next on my laptop or Chromebook, I convert my saved later articles to have the tag “BlogThis”.

    Now; as soon as I find something I wish to share I can immediately tag the article “BlogThis” and the wheels will immediately start turning. I have an IFTT recipe which will  scan Feedly for articles tagged with “BlogThis” and will then push them onto my blog www.RossGoodman.com. From there, there I then push the content from my blog onto Twitter, Facebook and LinkedIn.

    Expect to see more “real time” updates in the future!