More from Software and Tech stories from an Insider - iDiallo.com
It's easier for someone in my position, after working 20 years in this field to talk about morals. I can disagree with the choices my employer makes. In fact, I can walk away. But I remember the dilemma I felt I was in, earlier in my career. The lead developer stood behind me while I was working on a feature and asked me to make a div clickable. That's it. Technically, it's the simplest thing you can do. But, I hesitated. He watched from behind as I tabbed through every application on my computer, doing everything but what he asked. "Just make it clickable," he said again. I have made divs clickable a thousand times in my then short career, I knew that html standards were merely a suggestion. But this one rubbed me the wrong way. Instead I said, "I already have an anchor tag below, the div doesn't need to be clickable." "The fuck you're talking about? Just do it." We got into an argument, and I ended up doing exactly what he said. A little after I pushed my code, he revisited my code and added a slight delay to my click event using setTimeout to disconnect the user action to what occurred after. If what I'm saying sounds a bit vague, that's how it was framed at a time. We had disconnected the technical ask, clickable div, from the actual feature to make the whitespace on the page clickable. Let me make it even clearer. Have you ever used a coupon website? You buy something on a website, let's say nike.com. When you get to the checkout cart, they have a little box that says "enter coupon code for a discount". So you scour the web to find a coupon that will help you pay less for your purchase. You land on our website where we offer a dozen coupons, but they are all partially hidden. The section says "click to reveal". When you click, some random popup appears, then the code is revealed for you to copy and paste and you go on your merry way with a discount. Everything sounds fine so far. What you don't know is that the coupon website gets credit for the sale. A portion of the price you paid goes straight into our pocket, whether you got a discount or not. We get credit from the referral, that's how affiliate marketing works. So why is making the div clickable, or the whitespace clickable a big deal? Because it captures accidental clicks. The company doesn't care if they help you find a good discount or not. In fact, they don't even care if the code works. They just want you to click and for that popup to open so they can get the credit. My task was framed as a technical request, instead of outright saying "trick the user into clicking." When the Honey scandal surfaced, I was confused why it was news anyway. Deception in affiliate marketing is a standard. Very often we disconnect our work from the real world consequences. Back in 2019 I met this girl at a friend’s birthday party. She worked at a bank in the software development sector, and told me about the project on which she’d been working. Their system collected data from various sources on people applying for financial services (e.g.: loans) and would indicate if someone was eligible, or raise a red flag. In the latter case they would have to deal with substantial additional bureaucracy, and often times would not be able to access these services anyway. She seemed quite proud of her work, and told me her team had demoed it the previous week in front of the whole office. They’d shown the report that the system generated for each person in the team. In her case, the system flagged her as “dangerous”, and she was not eligible for a loan. Her grandmother was from Iran, and because there’s frequently cases of money laundering or other irregularities in Iran, she’s immediately flagged too. Despite her being a Dutch citizen, born and raised in the Netherlands, and working in this Dutch financial institution, her bloodline was “too risky” for her employer to lend her money.1 I was confused, and honestly, heartbroken. This person was telling me how proud they were of their work, building a system that discriminates them based on their bloodline. I questioned what she thought about it, and she explained “well, the rules are there…” —she paused a few times— “the rules are there…”. I remained silent and she eventually finished her sentence after repeating it a few times “the rules are there to protect us”. Even though this was six years ago, every time I remember this situation it brings me great sadness. Here was a person who’d worked hard to build a system which would discriminate against them, and yet stood proudly defending their work. We don't see it as our responsibility to question the system. We are happy to do the work as long as we don't see the immediate consequences. I feel this whenever I see flock cameras mounted in the street. Someone had to design them, write the code, test them, ensure that they work to spec. Someone had to install them. These are people doing a good technical job, yet I'm sure they wouldn't be happy if they were tracked by the very system. This is why I was mad when Anthropic wrote their manifesto saying they are against tracking citizens, as opposed to non citizens. You don't get one without the other. We track everyone then discriminate later. This is why I'm against age verification systems, no matter how technically impressive the solution is. Because in the end, I'm the one who has to upload my ID to a system that I cannot trust. With AI, it's even easier to disconnect your day to day work from its consequences. I understand it's much easier to ignore these things when you are just getting started in your career, but eventually, you have to voice your opinion.
During the pandemic, we completed one of our largest projects at work. To celebrate, since we couldn't meet in person, we all ordered food on DoorDash and played an online escape room game together. We were on a Zoom call, helping each other out and having fun. The first challenge was to escape a jail cell. To escape, each of us had to find clues in our own cell to figure out how to open the doors. We each had to find an object that solved a piece of the puzzle, and once we put them all together, the door would open. As a first challenge, it was easy enough. Everyone found a brightly colored object in their room and described it to the team so we could piece it together. Everyone but one team member. "Come on, read it, man, we can win this." He froze. Someone jumped in to help: "Mine was the most obvious green object in the room. Just look for something bright. Maybe blue, or orange, something that seems out of place." He didn't respond. He just sat there, frozen on camera. We figured he was having internet connectivity issues. We waited a good five minutes before he finally found it, and we moved on to the next level. I didn't think much of that day. We finished the game, we had fun, it was great. He waited until our next one-on-one to explain what had actually happened. He panicked, and he was embarrassed. It turned out he was colorblind. We were yelling random color names at him, and he couldn't, for the life of him, see any of them. As far as I can tell, I'm not colorblind, and it never would have occurred to me that this was something to account for. Just last week, I learned about Vehicle Motion Cues on the iPhone, a feature that helps reduce motion sickness. I don't think I've ever experienced motion sickness myself, or at least never in a car. Watching a blind person navigate a website was eye-opening for me. I realized that many of my past design choices would have worked against their experience without my ever knowing it. The same goes for someone navigating a computer entirely by voice. I recently rediscovered Windows Speech Recognition, which I found pretty annoying for my own needs. But for someone who relies on it for all of their computer use, it's an essential tool. A coworker once mentioned, almost in passing, that she struggles to read certain fonts because of dyslexia. Tight letter spacing and low-contrast text make some of our internal tools nearly unreadable to her. I had picked those fonts because they looked good on a demo slide. It had never crossed my mind that a font choice could be the difference between someone reading a document easily and someone giving up on it entirely. In some of our zoom calls, a teammate would often ask if he could do audio only before the call. While it didn't bother me at all, the managers kept insisting on everyone turning on their cameras. But after he used the camera for a few minutes, his connection would start dropping. I just assumed he had slow Internet. But the reality was he was located in a rural area and he relied entirely on his phone's hotspot to connect to the internet. The zoom call was using up all his data in minutes. None of these problems were problems for me. That's exactly what made them invisible. Unless you are experiencing these issues, there's little reason to ever notice them. We tend to design our tools, our meetings, and our expectations around our own experience of the world, and then mistake that experience for the default. It takes a colorblind teammate freezing on a call, or a friend who can't ride in the passenger seat without getting sick, to remind us that "normal" was only ever normal for us. You can never anticipate every invisible problem in advance, that's impossible. But at the very least, we should remember that our own experience is rarely the default. We should be a bit more curious on how others experience the tools we build.
Amazon has been accused several times for ripping off merchants on its platform. And every single time they denied any wrongdoing. A merchant, or anyone really, can create a product (or source it from China), then resell it on amazon. Amazon is the service provider, and hosts all the metrics concerning the products. If Amazon themselves were in the business of creating and selling products, then that creates a potential of conflict of interest. Because they have the data of all products that sell and sell well. They could replicate that success without doing any further research since the merchant has already confirmed the existence of demand. It's not surprising that Amazon Basics quickly became the best selling "private-label brand" on Amazon. They already know what sells because they have access to the data. Yet they continued to deny it, and state that they only ever use publicly available data from sellers. An Amazon spokesperson said the company believes the allegations are "factually incorrect and unsubstantiated," adding that Amazon strictly prohibits the "use or sharing of non-public, seller-specific data for the benefit of any seller, including sellers of private brands." Yet the results are right there for all to see. If you sell any product through Amazon, you are exposing your company's operations to them. If you want to keep that information to yourself, then you don't get to reach your customers, which in reality are Amazon's customers. If you want to buy something online, and get it shipped as quickly as possible, then Amazon is a blessing. Most often than not, you are not buying the product directly from Amazon. An independent store or vendor with a presence on Amazon will fulfill your order. The seller only has minor identifying characteristics on the platform. On the search result page, the space designated to the seller is small and insignificant. The customer has very few reminders that products are offered by anyone but Amazon. (Although if you want to dispute a sale, you are starkly reminded that the item is from a 3rd party vendor.) So there is no surprise when companies embrace AI internally, they are putting themselves at the risk of sharing their product with their competitors. Maybe the most obvious example is when Antropic came up with Claude Design. A tool to help users generate designs, wireframes, etc. Kinda like Figma. That's not a problem on its own, but when Antropic's chief product officer sits on Figma's board of directors, you can't say that there isn't a conflict of interest there. In fact, the chief product officer resigned from the board merely days before Claude Design was announced. He basically extracted all value from Figma then resigned. Figma's AI features are built on top of Claude. So Anthropic literally pulled an Amazon Basic on Figma. When companies force their own employees to use AI to do their day to day work, they are basically asking employees to upload company data to a 3rd party that may become a competitor. Sure something in the contract clause says that the AI company won't train on enterprise customer data, but nothing stops them from peaking at successful product data. Whenever someone tells me that they used AI to build an app and boast of its values or uniqueness, I want to remind them that if you can just prompt-create a product, so can the AI provider. In fact, they might have better resources to create a competing product if it displays any sign of success (see Figma). While it looks like plenty of people are benefitting from AI today, all this information is being shared with AI providers. We are giving them full access to our thought process. When you include them in your workflow, you are basically providing them with a step by step approach on how to do your job. Don’t be surprised when you see a native Antropic/OpenAi project management application suite. Or a CRM, or any software that is trying to integrate with AI and may experience success. A few years back, when I worked in Customer Service Automation, we discovered that most companies used Zendesk to manage their customer service. Since customers mainly contacted support via email, an intentional database had been built that tracked users through their shopping experience throughout the web. While so much could be done with that data, like identifying “problematic” customers, or recommending products based on their history, we ended up finding something more helpful. We could easily detect a pattern of issues for certain shipping carriers. We could see when UPS was having delays in certain cities, or when Fedex was having technical issues when updating the last mile status. None of these things were features designed or provided by anyone. However, having access to businesses’ data gave us insight where we had none before. That became a feature for us, only because we were not competitors to all these online retailers. When you expose your company's internal data to a potential competitor, don’t be surprised when they build a competing business to rival you.
Whenever I saw someone type a natural language query into Google, it made me cringe. "It's not a person," I would say. "Type like you're talking to a machine." This was especially true for programmers and it was before AI took over everything. Instead of "how do I write a function that reads a file?", I would suggest they use specific keywords, something that sounded more like machine language than conversation. "js function to read csv file" or "css gradient background property example." This got you better results. Even though Google was a sophisticated search engine, it was still doing a kind of keyword matching under the hood. But not anymore. You don't get any advantage from writing in "machine language." Google understands natural language just as well. In fact, even better. How is it that in 2026, I Google things less than ever? It's not that I know everything now. It's more that I don't want to call the friend who always talks too much. If the height of the Eiffel Tower ever comes up in conversation, I'll type "eiffel tower wiki" and click through to Wikipedia. I don't want to have a conversation about it. Googling something these days feels like Google is trying to join my private conversation. Where it used to be a tool for finding answers elsewhere, now it's a buddy who gives you an answer. And just as you're about to leave, it says, "hey, did you also know that..." There used to be a machine between me and the information I was looking for. It was good at its job. It sorted, ranked, then presented information. But now, the machine is constantly pushing information at me, watching my reaction, learning from it, and feeding me more, unsolicited. Before, information lived on the web and was hard to find. Today, information still exists, but it's buried under noise. Google no longer helps you find it, it just gives you an answer. That answer might be right or wrong, and right below it, in small print: "AI responses may include mistakes." You rarely get to verify whether the answer is correct, because almost no one clicks through to the source. I know this firsthand. More than three-quarters of my Google referral traffic has disappeared, while my search impressions keep climbing. So what's left to do? I could mourn the old Google, the simpler web. But as the title says, we aren't going back. This is the new reality, and we have to adapt. Rather than blindly embracing change, I think it's smarter to pick and choose. Just last week, I wrote about the small web still being alive. And it did exactly what its name suggests. It stayed small. There are other search engines built for people who want more control. DuckDuckGo. Kagi (my personal favorite). The habit of Googling everything is learned behavior and learned behaviors can be unlearned. What's harder to convey is that Google never presented us with facts, only sources and citations. The way the google answer is presented, we have the impression they are giving us undisputable truths. When everyone is sharing screenshots of the answer they got, all you can do is share a screenshot of the opposite answer you got. The source gets lost. That's where we are now. Skimming the average sentiment of a Reddit thread, or confirming something we already believed. This is the new reality. We're not going back to keyword matching. But I also don't have to accept the new way as the only way. Google has made its search box AI-first and that's their right, it's their product. But it's also my opportunity to try something different. We are not going back. So I might as well choose where I go next.
How is it possible that a feature I use every day, in an app I rely on daily, entirely offline, just disappeared from my phone? I use a fitness app. My metrics, such as steps, workout routines, heart rate, are collected from a wearable device like a smartwatch and sent to the app via Bluetooth. No third-party servers are involved in that transaction. The data lives on the phone. It costs the developer nothing to maintain, because there's nothing to maintain on their end. Then the app updates, just once, and that data is no longer accessible. Not because it was deleted or corrupted. Because the developer decided you now need to create an account on their servers to access information that already exists on your own device. That's why I have auto-update disabled on every device I own. Some of the apps on my phone are older than my children. You couldn't download them today even if you wanted to. The developer no longer offers that version. One of my apps is a single screen that displays information based on GPS data and compass orientation. I downloaded it in 2014. I've switched phones twice since then, and each time I've made sure to carry that app with me. I didn't keep it out of nostalgia, and because I have a hard time letting go. I kept it because the current version has three ads crammed onto that single screen. A full-screen ad hijacks the display at random intervals, complete with one of those countdown timers that slows down as it approaches zero. And of course, there are notifications now. None of that is for my benefit. I just need that one screen. Open the app, read the information, put it away. You might say I'm being cheap. That if I've used the app for over a decade, I clearly value it. So I should pay for the subscription and lose the ads. Fair point. But I have the old version. It was free, had no ads, and worked flawlessly. No future version can improve that. On top of it, those ads expose me. Advertising is one of the most common attack vectors in mobile security. Malvertising is a real thing. Updating to the ad-supported version wouldn't make my phone more secure. I don't update apps unless I've read about a specific vulnerability. Even then, I'll often delete the app rather than update it. I can't accept software that changes arbitrarily, especially when those changes almost never benefit me and almost always serve someone's bottom line. As a developer myself, I have the advantage of actually reading changelogs. When an update says "bug fixes," that's not a reason for me to act, unless I've encountered those bugs personally. Every user engages with a different 20% of an app's features. Someone else's bugs may never be mine. And why do developers push account creation so aggressively? Because your account is the product. An account means data. Data means third-party revenue. Every update is a decision point for me. It requires me to set aside time, read about the changes, and think about what I'm about to embark into. My workflow matters. My data matters. My time matters. If a developer breaks what worked for me without a compelling reason, I'll find another app that respects those things. There's always one out there, probably one that hasn't been improved yet.
More in technology
Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry. We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’
You want less of them. That’s the reason. You may find that it’s too hard to stop people from doing the thing, literally blood, sweat, and tears trying to prosecute people, but that’s a different thing.
Solitaire Alone Together I made a new game. It's called Solitaire Alone Together. It's Windows 98 solitaire, but you can play with everyone else on the internet. Read the full post on my blog! Here's a raw link, if you need it: https://eieio.games/blog/solitaire-alone-together
This post is a living diary of all the times I messed up something with my website in a funny way. I value those who have the confidence to own their mistakes and share the learning with others, and so this is me doing just that! That Time I Accidentally Made a Tarpit That Time I Accidentally Made Really Large Headers That Time I Accidentally Made a Tarpit Back to Top A "tarpit" is an unofficial term used in computing to describe an intentionally slow response to a request. In these modern times many people are using tarpits as a way to combat the relentless theft of data by AI companies, although there's little to no evidence of that actually being in any way effective. I don't use tarpits, at least not intentionally, but there was that one time when I accidentally created a tarpit and trapped all visitors in it. As I've shared previously, I refuse connections from IP addresses that are blocked or belong to a blocked subnet, and I enforce this firewall during the TCP handshake. The logic here is straightforward: there's no reason to waste resources doing a TLS handshake, accepting an HTTP request, and then rejecting the connection if I already know I'm going to reject it at the earliest step. At the time, the code worked like this: the HTTP server would repeatedly call the Accept() function below expecting a new connection. I've added some comments to help explain the logic. func (l *firewallListener) Accept() (net.Conn, error) { // Accept the connection from the TCP listener. This blocks until there is a connection to accept or the listner was closed. conn, err := l.l.AcceptTCP() if err != nil { return conn, err } // Separate the IP address out from the remote address (which includes the port) ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } // Check if it's blocked, if so close the connection and return a refuseError if IsBlocked(ip, true) { conn.Close() return nil, &refuseError{} } // Otherwise return the connection on to the HTTP server return conn, nil } If the incoming connection was from a blocked IP then I'd return a refuseError. I need to use a specific error interface because the HTTP server will halt if it encounters a non-temporary error from the call to Accept(), so I need to return an error that satisfies the definition of a temporary error. I defined refuseError like this: type refuseError struct{} func (e *refuseError) Error() string { return "." } func (e *refuseError) Timeout() bool { return true } func (e *refuseError) Temporary() bool { return true } func (e *refuseError) Is(err error) bool { return err == context.DeadlineExceeded } This did accomplish the goal of rejecting connections before the TLS handshake for blocked addresses, but it had one really unintended and difficult to track down side-effect. Accepting connections is done serially, after which servers typically then process that request on a dedicated thread (or in Go's case a goroutine). This means that any delays during the accept loop will block all incoming connection. What I had missed while reviewing the code for Go's HTTP server is that when it receives a temporary error from Accept() is that while it doesn't abort, it does sleep for up to a maximum of 1 second. This sleep blocks the entire server for all incoming connections. You can see a trimmed copy of the code that does this below, with some marks I've added which I will explain. // src/net/http/server.go // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. for { // (1) rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } // (2) if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } s.logf("http: Accept error: %v; retrying in %v", err, tempDelay) // (3) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := s.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return // (4) go c.serve(connCtx) } At mark 1 the server calls the Accept() function, this is the exact function that I defined above where I might return a temporary error. At mark 2 it checks if an error was returned, and if so if that error is temporary. If there was a temporary error, at mark 3 it sleeps for an increasing amount of time up-to 1 second, otherwise, at mark 4 it processes the connection on a dedicated goroutine, which allows the server to accept the next connection. I'm not entirely sure why the Go developers added this sleep delay and the change when it was introduced doesn't provide any meaningful insight. Regardless, it caused significant latency connecting to my website when a flood of rejected requests was coming in. It just goes to show how important it is to write meaningful commit messages, because you never know when somebody might come back years later wondering "why was this done?". I sure home I don't come to eat those words later. Coincidentally, you can actually see this happening if you look carefully at one of the metric graphs I shared in my first post about my server's security model: Securing My Web Infrastructure. This is the graph I shared in that blog post and while I didn't know it at the time, the fact that these request spikes all cap-out at around 60 requests per minute was not a coincidence. These requests were not being made with a limit in mind, attackers rarely ever care about things like that, instead it the accidental tarpit I had created. The downside to this was that while the malicious requests were being rate-limited, all requests were being rate-limited, up to a point of taking so long they timed out. The Fix Fixing the issue was relatively straightforward enough. Instead of returning a temporary error to the HTTP server during the accept loop, just don't return anything at all and wait for the next valid connection. func (l *firewallListener) Accept() (net.Conn, error) { for { conn, err := l.l.AcceptTCP() if err != nil { return conn, err } ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } if IsBlocked(ip, true) { conn.SetLinger(0) conn.Close() continue } return conn, nil } } Now, when the HTTP server calls Accept(), the only time it returns is with a connection from an IP that isn't blocked, or if there genuinely is an error. No more sleep delays, no more excessive timeouts. That Time I Accidentally Made Really Large Headers Back to Top For about 10 years now all major browsers have support for a security feature known as a Content Security Policy or CSP. A CSP is an HTTP header provided by the server that instructs the browser on where it can load assets from, this could be scripts, images, stylesheets, fonts, etc. The objective of using a CSP is to prevent against injected HTML that tries to load assets, such as a malicious Javascript file, from a remote source. With so much user-provided content being available online, it's very possible for this to happen without an attacker compromising the entire web server. CSP protects against that by saying "scripts can only be loaded from these domains". That's a really simplified way of looking at it, anyways. My web server supports injecting the CSP header automatically, but before I go on I need to explain a little bit about the structure of my web server. When an incoming HTTP request is accepted (having passed all firewall checks and assertions), we look at the destination host for the request. This can either be the value of the Host header or as specified during the TLS handshake. We then look at a map of hosts to apps. Apps are just an interface that accept a few methods: type App interface { Cleanup() ReloadConfig() ServeHTTP(rw http.ResponseWriter, r *http.Request) Setup(dataDir string) error Shutdown() } One of the apps is the Proxy app, which is a reverse proxy - it accepts the incoming HTTP request and then proxies it on to another host. This is a very common design, especially with increasingly complex TLS setups. Because each app is unique to a host, and different hosts have different requirements for CSP rules, the proxy app includes a CSP preset that we use to build the header value, or skip it entirely. When the proxy app was going to copy an HTTP request to the downstream host, it would build the CSP header, however there was a slight bug... func (a *App) ServeHTTP(rw http.ResponseWriter, inRequest *ht2.Request) { // --snip -- if a.CSP != nil { a.CSP.ConnectSrc += " " + inRequest.Origin } CopyHttpRequest(inRequest, outRequest, rw, CopyHttpRequestOptions{ Origin: inRequest.Origin, Csp: a.CSP, Cors: a.CORS, AddHeaders: !a.SkipHeaders, UseHTTP3: a.UseHTTP3, InsecureTLS: a.InsecureTLS, }) } I'm really unsure as to what I was doing with the line to append to the ConnectSrc, but the impact is that I'm appending to a variable that lives on the App, rather than a variable that is per-request. This meant that every time there was a request to the app, any request at all, the origin would be appended to the header value. This went on for quite a long time unnoticed and unresolved, largely because I am constantly tweaking and tinkering with my web server, after all, it's how I made having a website fun again. Each time I restarted the server process, the header value would be reset, but only for it to continue to grow and grow. Eventually, after a period of being busy with other matters, the server process stayed running for long enough that the header value grew too large and HTTP clients began to reject it. There is no defined maximum for an HTTP header value, however most HTTP clients use 100KiB, which is perfectly reasonable, and this header value would continue to grow well beyond that. Diagnosing this issue turned out to be difficult as tools like Curl would fail with errors relating to entities being too large, but stopped short of saying what specifically. I eventually used openssl s_client to send an HTTP request by hand and observed my terminal window being filled with a domain name repeated thousands of times. Looking at the commit history, it was really unclear why I added the culprit lines of code. The commit message just says "Improved CSP support". It just goes to show how important it is to write - hey look it's those words I'm now having to eat! The Fix The fix was to just delete those three lines of code. Yup, it really was that simple, and fixing this bug actually made a larger positive impact than I had expected, as it was immediately clear when I fixed the bug by looking at outbound network bytes: So much traffic was being wasted on excessive header sizes. You might look at these mistakes I've made and think "wow, Ian, these are some obvious mistakes, I never would have made them!" to which I say "good for you!" with the utmost sarcasm and disdain. I enjoy making and refining software, and making anything means making mistakes along the way. Each time I make mistakes such as the ones above, I improve my skills of investigation, diagnosing, and repair. Skills that, judging by my peers in the industry, seemingly everyone is quickly willing to throw away because a robot does it "better" than you. Header Image: "Car accident on the Ffestiniog to Bala road. Nobody was hurt" by Geoff Charles, CC BY-SA 4.0, via Wikimedia Commons.
Back in 2021, I wired up data from Buienalarm and Buienradar through Node-RED and a big pile of Jinja2 templates to get a rain forecast graph on my Apple Watch: eight Unicode block characters showing the next two hours in 15-minute chunks, glanceable without unlocking the phone. It worked, and I used it every day […] The post Buienwatch, a custom Home Assistant integration for Buienradar and Buienalarm graphs on your Apple Watch appeared first on Style over Substance.