<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>RyanCheley.com</title><link href="https://ryancheley.com/" rel="alternate"></link><link href="https://ryancheley.com/feeds/atom" rel="self"></link><id>https://ryancheley.com/</id><updated>2026-04-06T00:00:00-07:00</updated><subtitle>My Place on the Internet</subtitle><entry><title>A Giant Pain in the Ass</title><link href="https://ryancheley.com/2026/04/06/a-giant-pain-in-the-ass/" rel="alternate"></link><published>2026-04-06T00:00:00-07:00</published><updated>2026-04-06T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2026-04-06:/2026/04/06/a-giant-pain-in-the-ass/</id><summary type="html">&lt;p&gt;Content Warning: This is a highly personal post about a cancer diagnosis.&lt;/p&gt;
&lt;p&gt;On Feb 16, 2026 I was 'prepping' for a routine colonoscopy that was scheduled for February 17th at about 1pm. For those of you unaware what is involved in 'prepping' don't google it, but just know that your …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Content Warning: This is a highly personal post about a cancer diagnosis.&lt;/p&gt;
&lt;p&gt;On Feb 16, 2026 I was 'prepping' for a routine colonoscopy that was scheduled for February 17th at about 1pm. For those of you unaware what is involved in 'prepping' don't google it, but just know that your Gastroenterologist wants the 'end' of your GI tract 'clean'&lt;/p&gt;
&lt;p&gt;This also involves a lot of not eating. So you can get hangry. Or at least most people do. I had felt something wasn't quite right for a while. Nothing I could really put my finger on, I just didn't feel right. So when it came time for the prep it turned out I wasn't really hungry anyway. I couldn't really eat the weekend before either, and had been having issues sleeping. I was stressed about what my colonoscopy would show. At least subconsciously I was worried.&lt;/p&gt;
&lt;p&gt;On the day of my colonoscopy the staff at the office were all really nice. I even got a "First Colonoscopy" sticker!&lt;/p&gt;
&lt;p&gt;I was wheeled into the procedure room, introduced to the doctor and told to look at the wall. The next thing I knew I was being wheeled out to the recovery room. I laid there for a few minutes and then I saw my wife Emily. I was still a bit groggy from the anesthesia but I was &lt;em&gt;so&lt;/em&gt; happy to see her. It was the best feeling.&lt;/p&gt;
&lt;p&gt;She came next to the bed I was laying on and the doctor came over. He let Emily know that she may want to sit down. She said she preferred to stand. The doctor then told me that during the procedure they found a tumor.&lt;/p&gt;
&lt;p&gt;You have cancer&lt;/p&gt;
&lt;p&gt;I let the phrase sink in ... "You have cancer" ... "I have cancer".&lt;/p&gt;
&lt;p&gt;The doctor was not very comfortable delivering this news. You could tell this wasn't the type of thing he was used to doing. Emily even heard him saying "at the other place I don't have to tell patients this".&lt;/p&gt;
&lt;p&gt;I think he tried his best to be positive about the diagnosis, but honestly it was a pretty shitty delivery. He kept saying things like "you're young" (at the time I was 47) ... "you're good looking" ... "you're married"&lt;/p&gt;
&lt;p&gt;I didn't really understand why any of that mattered.&lt;/p&gt;
&lt;p&gt;I have cancer&lt;/p&gt;
&lt;p&gt;He then proceeded to let me know that the tumor had likely been there for years, maybe five. Had asked me if I had any symptoms, was there anything that felt off. How could I have not known something was wrong. I have cancer and it's my fault I didn't know sooner.&lt;/p&gt;
&lt;p&gt;He also let me know that I'd need to have an ostomoy bag. For the rest of my life.&lt;/p&gt;
&lt;p&gt;I have cancer ... that's all I could hear.&lt;/p&gt;
&lt;p&gt;I cried. I cried in front of several people that I had never met before. I cried in front of my wife.&lt;/p&gt;
&lt;p&gt;I have cancer. And I don't know anything about it at this point other than it seems like everyone I know with colorectal cancer died from it.&lt;/p&gt;
&lt;p&gt;I have cancer.&lt;/p&gt;
&lt;p&gt;I'm going to die&lt;/p&gt;
&lt;p&gt;... those two phrases kept going through my head&lt;/p&gt;
&lt;p&gt;I cried.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As an aside, I told a friend of mine how my cancer diagnosis was delivered to me. This friend has had many cancer battles / scares during their life. I figured when I told them my story they would say it wasn't so bad. Turns out it was. Even they were like, "Holy Shit. That's awful!"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After a few minutes, and once Emily and I were a bit more able to see the world, I was wheeled out to our car. We drove home. We didn't say much. What is there to even say? I have cancer.&lt;/p&gt;
&lt;p&gt;We got home and although I could eat again, I wasn't hungry. I was afraid I'd never be hungry again.&lt;/p&gt;
&lt;p&gt;That night I couldn't sleep. Or the night after. Or the night after.&lt;/p&gt;
&lt;p&gt;The next day I had a follow up with a different GI doctor. He was basically like, "There's no new information. Why are you here?"&lt;/p&gt;
&lt;p&gt;But we had questions. What are the next steps. Who should we contact? What do we do? The one question I didn't dare ask, "Am I going to die?"&lt;/p&gt;
&lt;p&gt;For the next 16 days I lived in the grayest of gray areas. I could barely sleep, or eat. I lost about 8 pounds.&lt;/p&gt;
&lt;p&gt;Emily and I spent time working to make sure that all of our affairs were in order. Are all of the banking apps installed on your phone? Do you have all of the passwords? Does the car title need to be updated? The title for the house? How do we do our finances?&lt;/p&gt;
&lt;p&gt;We make a good team in that we each have our 'assignments'. We're pretty autonomous in those assignments. We'd talked about "cross training" on some of them,  especially the financial stuff, but there just never seemed to be the time.&lt;/p&gt;
&lt;p&gt;And now it felt like we didn't have the time but were going to have to make it. I felt like I was writing transition docs for leaving a job. But in this case I was afraid of the job I might be leaving.&lt;/p&gt;
&lt;p&gt;To use any other word than &lt;code&gt;brutal&lt;/code&gt; to describe these days wouldn't do justice to the way we felt. And even then it doesn't really begin to cover it.&lt;/p&gt;
&lt;p&gt;During that time we told a few people. A very few people. Telling people made it real. Telling people was like delivering a trauma to them. Telling people led to questions. Questions we didn't have the answer for. Brutal.&lt;/p&gt;
&lt;p&gt;I had nights where I would cry. Especially if I was alone. I have cancer, but there were things that still needed to happen. Emily had a major work event that she was responsible for. She had coworkers and friends she was able to rely on, but that didn't mean she didn't have to do things. Away from home. Away from me.&lt;/p&gt;
&lt;p&gt;My Aunt had the same cancer diagnosis I do and she was helpful and caring and loving and kind and all of the things you need from a family member. But she didn't know the future. She didn't know if I was going to die. And so when the words, "You're going to be fine" came from her, they were nice, but hollow. I have cancer. I might die. I am scared.&lt;/p&gt;
&lt;p&gt;On March 5th I met with a surgeon. Before meeting with the surgeon I needed to have an Abdominal CT scan done. It was completed about a week before I met with the surgeon. I had the results 2 days before meeting with the surgeon. I couldn't look at them. I didn't want to look at them.&lt;/p&gt;
&lt;p&gt;The day of the surgery consult came. He was going to tell me the next steps. From what I heard surgery was likely to be my next step. My wife and I went to the appointment, she's been going to all of my appointments with me.&lt;/p&gt;
&lt;p&gt;The staff were so nice and friendly and helpful. I started in one exam room and was moved to another exam room. My first thought was, "Oh no, was I in the 'you're going to be fine room' and got moved to the 'You're going to die' room? But the nurse let me know the reason for the move. A simple reason. No big deal. Except it was. It was the biggest deal. But she took the time to let me know the &lt;code&gt;why&lt;/code&gt; of the move.&lt;/p&gt;
&lt;p&gt;I went through the exam. Emily came back so the surgeon could talk to us.&lt;/p&gt;
&lt;p&gt;All of the fear, and horrible anticipation. What ever he said next we were going to work through it. We were going to figure it out.&lt;/p&gt;
&lt;p&gt;At the end of all of this, "we" might end up being just "she".&lt;/p&gt;
&lt;p&gt;And the doctor said ...&lt;/p&gt;
&lt;p&gt;It's actually not bad. We seem to have caught it early. We'll want to do chemo and radiation before reevaluating surgery.&lt;/p&gt;
&lt;p&gt;I cried. This time I cried because it was the first hope I'd had in almost 3 weeks. I cried because my birthday was in 2 days and I had friends I was going to hang out with and it will be an actual happy party and not a pre wake party.&lt;/p&gt;
&lt;p&gt;Since the surgeon I've seen a few more doctors. An oncologist and a radiation oncologist. Each appointment was mostly what one might expect. A brief conversation about potential side effects of the treatment. Which are pretty horrific if you think about them for too long. I try not to.&lt;/p&gt;
&lt;p&gt;My treatment will be 5 days a week for 5 1/2 weeks of radiation and chemotherapy. Reevaluation of the tumor for potential surgery 6 - 12 weeks after that.&lt;/p&gt;
&lt;p&gt;I'm sleeping better, but still not great. I eating better, but still not a ton.&lt;/p&gt;
&lt;p&gt;And then ... for a few weeks ... nothing. Paper work is getting processed and I'm waiting for an MRI. The important part about the MRI is that it will tell me what stage and grade my tumor is. Once that's completed and the results are read then all of the doctors will have what they need to allow me to officially begin my treatment.&lt;/p&gt;
&lt;p&gt;That being said, I have a tentative start date for my treatment. Unless my MRI shows something unexpected, I'll start my radiation and chemotherapy treatments on April 13. Officially. Fifty Five days from when I was told I had cancer to start of treatment. I'm not sure if this is a long time or not. It felt like a long time. A really long fucking time.&lt;/p&gt;
&lt;p&gt;As part of the treatment you go in for a prep session. During this session they fit you for the device that blasts your tumor with radiation. In my case they also gave me 3 tattoos. One on either side and one right below my belly button.&lt;/p&gt;
&lt;p&gt;I always thought my first tattoo would be of something way cooler 🤷🏻‍♂️&lt;/p&gt;
&lt;p&gt;Before my diagnosis I had some plans for this year. I was going to go to &lt;a href="https://us.pycon.org/2026/"&gt;PyCon US&lt;/a&gt;, &lt;a href="https://www.pyohio.org/2026/"&gt;PyOhio&lt;/a&gt;, and &lt;a href="https://2026.djangocon.us/"&gt;DjangoCon US&lt;/a&gt;. I even toyed with the idea of going to &lt;a href="https://pretix.northbaypython.org/nbpy/nbpy-2026/"&gt;North Bay Python&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I won't be able to do any of these. Although my treatment will be done by late May, I'm not sure I can commit to much travel. I'm not sure how I'll be feeling.&lt;/p&gt;
&lt;p&gt;Also, anywhere from 6 - 12 weeks after the end of my treatment I get re-evaluated for surgery. If the tumor is gone and the various docs feel like there's no risk, then surgery might not be required. If there is a risk, then surgery will be required.&lt;/p&gt;
&lt;p&gt;The outcome of the surgery will be a colostomy bag that is either temporary (about 6 months) or permanent.&lt;/p&gt;
&lt;p&gt;I'm less than 2 months into my cancer journey and there's still so much I don't know. Still so much that just can't be known. And honestly that's the hardest part.&lt;/p&gt;
&lt;p&gt;My prognosis is good. My family and I are optimistic. But there's still so much we can't know. We hope that this will be a 'blip' and that by 2027 or 2028 we can go back to what ever normal is. But we just can't know.&lt;/p&gt;
&lt;p&gt;One of the things I've really focused on over the last 2 months is trying to find the good things. I saw someone post on Mastodon (sorry, I can't find the original toot) about finding what they called glimmers. Those small things that make you happy.&lt;/p&gt;
&lt;p&gt;I try to do that every day. A song I haven't heard in a long time. A friendly face while I'm out and about. A text from a person I haven't heard from in a while. Going for a swim. These are all things that I was taking for granted. I will likely end up taking them for granted again. But for now, I am really trying to be more appreciative of them. I'm trying to be more present.&lt;/p&gt;
&lt;p&gt;Anyway, for those of you out there in that are 45+ and haven't gotten a colonoscopy. You should. We seem to have caught this early in the process. My prognosis is good. If someone hadn't told me I had cancer I would mostly have no idea.&lt;/p&gt;</content><category term="musings"></category><category term="cancer"></category></entry><entry><title>Migrating to Hetzner with Coolify</title><link href="https://ryancheley.com/2026/01/12/migrating-to-hetzner-with-coolify/" rel="alternate"></link><published>2026-01-12T00:00:00-08:00</published><updated>2026-01-12T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2026-01-12:/2026/01/12/migrating-to-hetzner-with-coolify/</id><summary type="html">&lt;h1&gt;What I did&lt;/h1&gt;
&lt;p&gt;A few weeks ago, I got to watch &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt; migrate &lt;a href="https://djangopackages.org/"&gt;DjangoPackages&lt;/a&gt; from &lt;a href="https://www.digitalocean.com/"&gt;DigitalOcean&lt;/a&gt; to &lt;a href="https://hetzner.cloud/?ref=gNVHuEvaKgAw"&gt;Hetzner&lt;/a&gt;&lt;sup id="sf-migrating-to-hetzner-with-coolify-1-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-1" class="simple-footnote" title="This is an affiliate link"&gt;1&lt;/a&gt;&lt;/sup&gt; using &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;. The magical world of Coolify made everything look just so ... easy. Jeff mentioned that one of the driving forces for the decision to go to Hetzner was the …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;What I did&lt;/h1&gt;
&lt;p&gt;A few weeks ago, I got to watch &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt; migrate &lt;a href="https://djangopackages.org/"&gt;DjangoPackages&lt;/a&gt; from &lt;a href="https://www.digitalocean.com/"&gt;DigitalOcean&lt;/a&gt; to &lt;a href="https://hetzner.cloud/?ref=gNVHuEvaKgAw"&gt;Hetzner&lt;/a&gt;&lt;sup id="sf-migrating-to-hetzner-with-coolify-1-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-1" class="simple-footnote" title="This is an affiliate link"&gt;1&lt;/a&gt;&lt;/sup&gt; using &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;. The magical world of Coolify made everything look just so ... easy. Jeff mentioned that one of the driving forces for the decision to go to Hetzner was the price, that is Hetzner is cheaper but with the same quality.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Aside&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To give some perspective, the table below shows a comparison of what you I had and what I pay(paid) at each VPS&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server Spec&lt;/th&gt;
&lt;th&gt;Digital Ocean Cost&lt;/th&gt;
&lt;th&gt;Hetzner Cost&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed Database with 1GB RAM, 1vCPU&lt;/td&gt;
&lt;td&gt;$15.15&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s-1vcpu-2gb&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s-1vcpu-1gb&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpx11&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;$4.99&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpx21&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;$9.99&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpx31&lt;/td&gt;
&lt;td&gt;NA&lt;/td&gt;
&lt;td&gt;$17.99&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With Digital Ocean I was paying about $72.50 per month for my servers. This got me 2 Managed Databases (@$15 each) and 4 Ubuntu servers (1 s-1vcpu-2gb and 3 s-1vcpu-1gb).&lt;/p&gt;
&lt;p&gt;Based on my maths for January I should see my Hetzner bill be about $61 with the only downside being that I have to 'manage' my databases myself ... however, with Digital Ocean I always felt like I was playing with house money because I didn't have the paid for backups. Now, with Hetzner, I have backups saved to an S3 bucket (and Coolify has &lt;a href="https://coolify.io/docs/knowledge-base/s3/aws"&gt;amazing docs&lt;/a&gt; for how to set this up!)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;End Aside&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Original State&lt;/h2&gt;
&lt;p&gt;I had 6 servers on Digital Ocean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 production web servers&lt;/li&gt;
&lt;li&gt;1 test web server&lt;/li&gt;
&lt;li&gt;1 managed database production server&lt;/li&gt;
&lt;li&gt;1 managed database test server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This cost me roughly $75 per month.&lt;/p&gt;
&lt;h2&gt;Current State&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2 production web servers&lt;/li&gt;
&lt;li&gt;1 test web server&lt;/li&gt;
&lt;li&gt;1 production database server&lt;/li&gt;
&lt;li&gt;1 test database server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This cost me roughly $63 per month.&lt;/p&gt;
&lt;h2&gt;Setting Up Hetzner&lt;/h2&gt;
&lt;p&gt;In order to get this all started I need to &lt;a href="https://hetzner.cloud/?ref=gNVHuEvaKgAw"&gt;create a Hetzner account&lt;/a&gt;&lt;sup id="sf-migrating-to-hetzner-with-coolify-2-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-2" class="simple-footnote" title="This is an affiliate link"&gt;2&lt;/a&gt;&lt;/sup&gt;. Once I did that I created my first server, a CPX11 so that I could &lt;a href="https://coolify.io/self-hosted/"&gt;install Coolify&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, I need to clean up my DNS records. Over the years I had DNS managed at my registrar of choice (&lt;a href="https://hover.com"&gt;hover.com&lt;/a&gt;) and within Digital Ocean. Hetzner has a DNS server, so I decided to move everything there. Once all of my DNS was there, I added a record for my Coolify instance and proceeded with the initial set up.&lt;/p&gt;
&lt;p&gt;In all I migrated 9 sites. They can be roughly broken down like this&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5 &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; sites&lt;/li&gt;
&lt;li&gt;3 &lt;a href="http://datasette.io/"&gt;Datasette&lt;/a&gt; sites&lt;/li&gt;
&lt;li&gt;2 &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt; site&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Coolify: setting up projects, environments, resources&lt;/h2&gt;
&lt;p&gt;Coolify has several concepts that took a second to click for me&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://coolify.io/docs/get-started/concepts#projects"&gt;Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coolify.io/docs/get-started/concepts#environments"&gt;Environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coolify.io/docs/get-started/concepts#resources"&gt;Resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It took me some time reading through the docs but once it clicked I ended up segregating my projects in a way that made sense to me. I also ended up creating 2 environments for each project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Production&lt;/li&gt;
&lt;li&gt;UAT&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Starting with Nixpacks&lt;/h2&gt;
&lt;p&gt;Initially I thought that Coolify would only support &lt;code&gt;Docker&lt;/code&gt; or &lt;code&gt;docker-compose&lt;/code&gt; files, but there is also an option for static sites, and &lt;a href="https://nixpacks.com/docs/getting-started"&gt;Nixpacks&lt;/a&gt;. It turns out that Nixpacks were exactly what I wanted in order to get started.&lt;/p&gt;
&lt;p&gt;NB: There is a note on the Nixpacks site that states&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"⚠️ Maintenance Mode: This project is currently in maintenance mode and is not under active development. We recommend using Railpack as a replacement."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, Railpack isn't something that Coolify offered so 🤷🏻‍♂️&lt;/p&gt;
&lt;p&gt;I have a silly Django app called &lt;a href="https://doestatisjrhaveanerrortoday.com/"&gt;DoesTatisJrHaveAnErrorToday.com&lt;/a&gt;&lt;sup id="sf-migrating-to-hetzner-with-coolify-3-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-3" class="simple-footnote" title="More details on why I have this site here"&gt;3&lt;/a&gt;&lt;/sup&gt; that seemed like the lowest risk site to start with on this experiment.&lt;/p&gt;
&lt;h3&gt;Outline of Migration steps&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Allow access to database server from associated Hetzner server, i.e. Production Hetzner server needs to access Production Digital Ocean managed database server, UAT Hetzner Web server needs access to UAT Digital Ocean managed database server&lt;/li&gt;
&lt;li&gt;Set up hetzner.domain.tld in DNS record, for example, hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/li&gt;
&lt;li&gt;Set up site in Coolify in my chosen Project, Environment, and Resource. For me this was Tatis, UAT, "Does Tatis Jr Have An Error UAT"&lt;/li&gt;
&lt;li&gt;Configure the General tab in Coolify. For me this meant just adding an entry to 'Domains' with &lt;code&gt;https://hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Configure environment variables &lt;sup id="sf-migrating-to-hetzner-with-coolify-4-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-4" class="simple-footnote" title="Considerations for Nixpacks. The default version of Python for Nixpacks is 3.11. You can override this with an environment variable NIXPACKS_PYTHON_VERSION to allow up to Python 3.13."&gt;4&lt;/a&gt;&lt;/sup&gt;. &lt;sup id="sf-migrating-to-hetzner-with-coolify-5-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-5" class="simple-footnote" title="Here, you need to make sure that your ALLOWED_HOSTS is hetzner.uat.doestatisjrhaveanerrortoday.com"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Hit Deploy&lt;/li&gt;
&lt;li&gt;Verify everything works&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;General&lt;/code&gt; &amp;gt; &lt;code&gt;Domains&lt;/code&gt; entry to have &lt;code&gt;https://hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;https://uat.doestatisjrhaveanerrortoday.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update DNS to have UAT point to Hetzner server&lt;/li&gt;
&lt;li&gt;Deploy again&lt;/li&gt;
&lt;li&gt;Wait ... for DNS propagation&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;https://uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; works&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;https://hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; from DNS and Coolify&lt;/li&gt;
&lt;li&gt;Deploy again&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;https://uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; still works&lt;/li&gt;
&lt;li&gt;Remove the GitHub Action I had to deploy to my Digital Ocean UAT server&lt;/li&gt;
&lt;li&gt;Repeat for Production, replacing &lt;code&gt;https://hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; with &lt;code&gt;https://hetzner.doestatisjrhaveanerrortoday.com&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Repeat the process for each of my other Django sites (all 4 of them)&lt;/p&gt;
&lt;h2&gt;Switching to Dockerfile/docker-compose.yaml&lt;/h2&gt;
&lt;p&gt;This worked great, but&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The highest version of Python with Nixpacks is Python 3.13&lt;/li&gt;
&lt;li&gt;The warning message I mentioned above about Nixpacks being in "Maintenance Mode"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, I had a Datasette / Django app combination that I wanted to deploy, but couldn't figure out how with NIXPACKS. While the Django App isn't where I want it to be, and I'm pretty sure there's a Datasette plugin that would do most of what the Django app does, I liked the way it was set up and wanted to keep it!&lt;/p&gt;
&lt;h3&gt;Writing the &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yaml&lt;/code&gt; files&lt;/h3&gt;
&lt;p&gt;I utilized Claude to assist with starting me off on my Dockerfile and &lt;code&gt;docker-compose.yaml&lt;/code&gt; files. This made migrating off of the NIXPACK a bit easier than I thought it would be.&lt;/p&gt;
&lt;p&gt;I was able to get all of my Django and Datasette apps onto a Dockerfile configuration but there was &lt;a href="https://ahl-data.ryancheleyc.com"&gt;one site&lt;/a&gt; I have that scrape game data from &lt;a href="https://www.theahl.com"&gt;TheAHL.com&lt;/a&gt; which has an accompanying Django app that required a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file to get set up&lt;sup id="sf-migrating-to-hetzner-with-coolify-6-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-6" class="simple-footnote" title="Do I need this set up? Probably not. I'm pretty sure there's a Datasette plugin that does allow for edits in the SQLite database, but this was more of a Can I do this, not I need to do this kind of thing"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;One gotcha I discovered was that the Coolify UI seems to indicate that you can declare your &lt;code&gt;docker-compose&lt;/code&gt; file with any name, but my experience was that it expected the file to be called &lt;code&gt;docker-compose.yaml&lt;/code&gt; not &lt;code&gt;docker-compose.yml&lt;/code&gt; which did lead to a bit more time troubleshooting that I would have liked!&lt;/p&gt;
&lt;h2&gt;Upgrading all the things&lt;/h2&gt;
&lt;p&gt;OK, now with everything running from Docker I set about upgrading all of my Python versions to Python 3.14. This proved to be relatively easy for the Django apps, and a bit more complicated with the Datasette apps, but only because of a decision I had made at some point to pin to an alpha version 1.0 of Datasette. Once I discovered the underlying issue and resolved it, again, a walk in the park to upgrade.&lt;/p&gt;
&lt;p&gt;Once I was on Python 3.14 it was another relatively straight forward task to upgrade all of my apps to Django 6.0. Honestly, Docker just feels like magic given what I was doing before and just how worried I'd get when trying to upgrade my Python versions or my Django versions.&lt;/p&gt;
&lt;h2&gt;Migrating database servers&lt;/h2&gt;
&lt;p&gt;Now I've been able to wind down all of my Web Servers, the only thing left is my managed database servers. In order to get them set up I set up &lt;a href="https://www.pgadmin.org/"&gt;pgadmin&lt;/a&gt; (with the help of Coolify) so that I didn't have to drop into psql in the terminal on the servers I was going to use for my database servers.&lt;/p&gt;
&lt;p&gt;Once that was done I created backups of each database from Production and UAT on my MacBook so that I could restore them to the new Hetzner servers. To get the backup I ran this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;--rm postgres:17 pg_dump "postgresql://doadmin:password@host:port/database?sslmode=require" -F c --no-owner --no-privileges &amp;gt; database.dump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I did this for each of my 4 databases. Why did I use the &lt;code&gt;docker run --rm postgres:17 pg_dump&lt;/code&gt; instead of just &lt;code&gt;pg_dump&lt;/code&gt;? Because my MacBook had Postgres 16 while the server was on Postgres 17 and this was easier than upgrading my local Postgres instance.&lt;/p&gt;
&lt;h3&gt;Starting with UAT&lt;/h3&gt;
&lt;p&gt;I started with my test servers first so I could break things and have it not matter. I used my least risky site (tatis) first.&lt;/p&gt;
&lt;p&gt;The steps I used were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create database on Hetzner UAT database server&lt;/li&gt;
&lt;li&gt;Restore from UAT on Digital Ocean&lt;/li&gt;
&lt;li&gt;Repeat for each database&lt;/li&gt;
&lt;li&gt;Open up access to Hetzner database server for Hetzner UAT web server&lt;/li&gt;
&lt;li&gt;Change connection string for &lt;code&gt;DATABASE_URL&lt;/code&gt; for tatis to point to Hetzner server in my environment variables&lt;/li&gt;
&lt;li&gt;Deploy UAT site&lt;/li&gt;
&lt;li&gt;Verify change works&lt;/li&gt;
&lt;li&gt;Drop database from UAT Digital Ocean database server&lt;/li&gt;
&lt;li&gt;Verify site still runs&lt;/li&gt;
&lt;li&gt;Repeat for each Django app on UAT&lt;/li&gt;
&lt;li&gt;Allow access to Digital Ocean server from only my IP address&lt;/li&gt;
&lt;li&gt;Verify everything still works&lt;/li&gt;
&lt;li&gt;Destroy UAT managed database server&lt;/li&gt;
&lt;li&gt;Repeat for prod&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the 8 sites / databases this took about 1 hour. Which, given how much needed to be done was a pretty quick turn around. That being said, I spent probably 2.5 hours planning it out to make sure that I had everything set up and didn't break anything, even on my test servers.&lt;/p&gt;
&lt;h2&gt;Backups&lt;/h2&gt;
&lt;p&gt;One thing about the Digital Ocean managed servers is that backups were an extra fee. I did not pay for the backups. This was a mistake ... I should have, and it always freaked me out that I didn't have them enabled. Even though these are essentially hobby projects, when you don't do the right thing you know it.&lt;/p&gt;
&lt;p&gt;Now that I'm on non-managed servers I decided to fix that, and it turns out that Coolify has a &lt;a href="https://coolify.io/docs/knowledge-base/s3/aws"&gt;really great tutorial&lt;/a&gt; on how to set up an AWS S3 bucket to have your database backups written to.&lt;/p&gt;
&lt;p&gt;It was so easy I was able to set up the backups for each of my databases with no fuss.&lt;/p&gt;
&lt;h2&gt;Coolify dashboard thing&lt;/h2&gt;
&lt;p&gt;I mentioned above that I upgraded all of apps to Python 3.14 and Django versions to 6.0. For all of the great things about Coolify, trying to find this information out on a high level is a pain in the ass. Luckily they have a &lt;a href="https://coolify.io/docs/api-reference/authorization"&gt;fairly robust API&lt;/a&gt; that allowed me to vibe code a script that would output an HTML file that showed me everything I needed to know about my Applications with respect to Python, Django, and Datasette versions. It also helped me know about my database backup setups as well&lt;sup id="sf-migrating-to-hetzner-with-coolify-7-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-7" class="simple-footnote" title="There are a few missing endpoints, specifically when it comes to Service database details"&gt;7&lt;/a&gt;&lt;/sup&gt;!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Final Image of Tatis Status" src="https://ryancheley.com/images/coolify-dashboard.png"&gt;&lt;/p&gt;
&lt;p&gt;This is an example of the final state, but what I saw was some Sites on Django 4.2, others on Python 3.10 and ... yeah, it was a mess!&lt;/p&gt;
&lt;p&gt;I might release this as a package or something at some point, but I'm not sure that anyone other than me would want to use it so, 🤷‍♂️&lt;/p&gt;
&lt;h2&gt;What this allows me to do now&lt;/h2&gt;
&lt;p&gt;One of the great features of Coolify are &lt;a href="https://coolify.io/docs/applications/index#preview-deployments"&gt;Preview Deployments&lt;/a&gt; which I've been able to implement relatively easily&lt;sup id="sf-migrating-to-hetzner-with-coolify-8-back"&gt;&lt;a href="#sf-migrating-to-hetzner-with-coolify-8" class="simple-footnote" title="one of my sites doesn't like serving up the deployment preview with SSL, but I'm working on that!"&gt;8&lt;/a&gt;&lt;/sup&gt;. This allows me to be pretty confident that what I've done will work out OK. Even with a UAT server, sometimes just having that extra bit of security feels ... nice.&lt;/p&gt;
&lt;p&gt;One thing I did (because I could, not because I needed to!) was to have a PR specific database on my UAT database server. Each database is called {project}_pr and is a full copy of my UAT database server. I have a cron job set up that restores these databases each night.&lt;/p&gt;
&lt;p&gt;I used Claude to help generate the shell script below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# Usage: ./copy_db.sh source_db container_id [target_db]&lt;/span&gt;
&lt;span class="c1"&gt;# If target_db is not provided, it will be source_db_pr&lt;/span&gt;

&lt;span class="nv"&gt;source_db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;container_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;target_db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Validate required parameters&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error: Source database name required"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; source_db container_id [target_db]"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error: Container ID required"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; source_db container_id [target_db]"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Set default target_db if not provided&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;target_db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;source_db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_pr"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Dynamic log filename&lt;/span&gt;
&lt;span class="nv"&gt;log_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;source_db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_copy.log"&lt;/span&gt;

&lt;span class="c1"&gt;# Function to log with timestamp&lt;/span&gt;
log_message&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Starting database copy: &lt;/span&gt;&lt;span class="nv"&gt;$source_db&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Container: &lt;/span&gt;&lt;span class="nv"&gt;$container_id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Drop existing target database&lt;/span&gt;
log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dropping database &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt; if it exists..."&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;postgres&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DROP DATABASE IF EXISTS &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✓ Successfully dropped &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✗ Failed to drop &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Create new target database&lt;/span&gt;
log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Creating database &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;postgres&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE DATABASE &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✓ Successfully created &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✗ Failed to create &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Copy database using pg_dump&lt;/span&gt;
log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Copying data from &lt;/span&gt;&lt;span class="nv"&gt;$source_db&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sh&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pg_dump -U postgres &lt;/span&gt;&lt;span class="nv"&gt;$source_db&lt;/span&gt;&lt;span class="s2"&gt; | psql -U postgres &lt;/span&gt;&lt;span class="nv"&gt;$target_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✓ Successfully copied database"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✗ Failed to copy database"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Database copy completed successfully"&lt;/span&gt;
log_message&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Log file: &lt;/span&gt;&lt;span class="nv"&gt;$log_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Again, is this &lt;em&gt;strictly&lt;/em&gt; necessary? Not really. Did I do it anyway just because? Yes!&lt;/p&gt;
&lt;h2&gt;Was it worth it&lt;/h2&gt;
&lt;p&gt;Hell yes!&lt;/p&gt;
&lt;p&gt;It took time. I'd estimate about 3 hours per Django site, and 1.5 hours per non-Django site. I'm happy with my backup strategy, and preview deployments are just so cool. I did this mostly over the Christmas / New Year holiday as I fought through a cold.&lt;/p&gt;
&lt;p&gt;Another benefit of being on Coolify is that I'm able to run&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://booklore.org/"&gt;Booklore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freshrss.org/index.html"&gt;FreshRSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What's next?&lt;/h2&gt;
&lt;p&gt;Getting everything set up to be &lt;em&gt;mostly&lt;/em&gt; consistent is great, but there are still some differences that exist between each Django site that don't need to when it comes to the &lt;code&gt;Dockerfile&lt;/code&gt; that each site is using.&lt;/p&gt;
&lt;p&gt;I also see the potential to have better alignment on the use of my third party packages. Sometimes I chose package X because that's what I knew about at the time, and then I discovered package Y but never went back and switched it out where I was using package X before.&lt;/p&gt;
&lt;p&gt;Finally, I really want to figure out the issue of https on &lt;strong&gt;some&lt;/strong&gt; of the preview deployments.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-1"&gt;This is an affiliate link &lt;a href="#sf-migrating-to-hetzner-with-coolify-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-2"&gt;This is an affiliate link &lt;a href="#sf-migrating-to-hetzner-with-coolify-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-3"&gt;More details on why I have this site &lt;a href="https://ryancheley.com/2021/05/31/how-does-my-django-site-connect-to-the-internet-anyway/"&gt;here&lt;/a&gt; &lt;a href="#sf-migrating-to-hetzner-with-coolify-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-4"&gt;Considerations for Nixpacks. The default version of Python for Nixpacks is 3.11. You can override this with an environment variable &lt;code&gt;NIXPACKS_PYTHON_VERSION&lt;/code&gt; to allow up to Python 3.13. &lt;a href="#sf-migrating-to-hetzner-with-coolify-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-5"&gt;Here, you need to make sure that your ALLOWED_HOSTS is &lt;code&gt;hetzner.uat.doestatisjrhaveanerrortoday.com&lt;/code&gt; &lt;a href="#sf-migrating-to-hetzner-with-coolify-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-6"&gt;Do I need this set up? Probably not. I'm pretty sure there's a Datasette plugin that does allow for edits in the SQLite database, but this was more of a Can I do this, not I need to do this kind of thing &lt;a href="#sf-migrating-to-hetzner-with-coolify-6-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-7"&gt;There are a few missing endpoints, specifically when it comes to Service database details &lt;a href="#sf-migrating-to-hetzner-with-coolify-7-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-migrating-to-hetzner-with-coolify-8"&gt;one of my sites doesn't like serving up the deployment preview with SSL, but I'm working on that! &lt;a href="#sf-migrating-to-hetzner-with-coolify-8-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="devops"></category><category term="coolify"></category><category term="hetzner"></category><category term="django"></category><category term="datasette"></category></entry><entry><title>Year in Review 2025</title><link href="https://ryancheley.com/2025/12/31/year-in-review-2025/" rel="alternate"></link><published>2025-12-31T00:00:00-08:00</published><updated>2025-12-31T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-12-31:/2025/12/31/year-in-review-2025/</id><summary type="html">&lt;p&gt;I was hoping to have this written and posted last week, but for Christmas this year Santa brought me a cold which knocked me on my butt for a few days.&lt;/p&gt;
&lt;p&gt;I had done a bit of prep, but wow, when I look back at 2025 it was a pretty …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I was hoping to have this written and posted last week, but for Christmas this year Santa brought me a cold which knocked me on my butt for a few days.&lt;/p&gt;
&lt;p&gt;I had done a bit of prep, but wow, when I look back at 2025 it was a pretty big year for me personally.&lt;/p&gt;
&lt;h2&gt;Professional&lt;/h2&gt;
&lt;p&gt;I celebrated 17 years at my &lt;a href="https://www.mydohc.com"&gt;current employer&lt;/a&gt;. While this isn't a nice round number sort of anniversary, about 6 months before the actual anniversary date I was promoted to an Associated Vice President and joined the Senior Management Team. This has been a goal of mine since about 2010 and after a lot of hard work (and honestly more than a bit of good luck) I "made it".&lt;/p&gt;
&lt;p&gt;In addition to the promotion at work, I also helped to lead a multi department team to a successful upgrade for a major application AND helped to lead a major network migration for our EHR that went really well. Two major projects accomplished in the same calendar year was a pretty good feeling.&lt;/p&gt;
&lt;p&gt;We also do annual employee satisfaction surveys and my department had a 96% satisfaction rating. This is a really good feeling as a leader. We get shit done AND people are happy to do it!&lt;/p&gt;
&lt;p&gt;Since 2021 my department has consistently scored above 90%. This isn't just me though! I have a great management team that helps to make this happen.&lt;/p&gt;
&lt;p&gt;Over this same time period I've had 7 people leave the department&lt;sup id="sf-year-in-review-2025-1-back"&gt;&lt;a href="#sf-year-in-review-2025-1" class="simple-footnote" title="The department is 13 people"&gt;1&lt;/a&gt;&lt;/sup&gt;. Five of them because of retirement. I really like that where I work is a place you retire at more often than not! That, along with the high satisfaction rates, suggest that my management team and I are doing something right.&lt;/p&gt;
&lt;h2&gt;Django and Python&lt;/h2&gt;
&lt;p&gt;On the Django and Python side it was also a really big year. In February I &lt;a href="https://youtu.be/FBMg2Bp4I-Q?si=tzHCWboxaEa8vEh3"&gt;spoke&lt;/a&gt; at &lt;a href="https://2025.pycascades.com/"&gt;PyCascades&lt;/a&gt; in Portland, Oregon.&lt;/p&gt;
&lt;p&gt;In September I &lt;a href="https://youtu.be/aZwKCo5kwJU?si=eel7u86Czjzl-CsV"&gt;spoke&lt;/a&gt; at &lt;a href="https://2025.djangocon.us/"&gt;DjangoCon US&lt;/a&gt; in Chicago, Illinois. This was my THIRD talk at DjangoCon US&lt;sup id="sf-year-in-review-2025-2-back"&gt;&lt;a href="#sf-year-in-review-2025-2" class="simple-footnote" title="DCUS 2023, DCUS 2024"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I was also active with &lt;a href="https://django-commons.org/"&gt;Django Commons&lt;/a&gt; on the admin team, was a &lt;a href="https://djangonaut.space/"&gt;Djangonaut.Space&lt;/a&gt; &lt;a href="https://github.com/djangonaut-space/program/blob/main/navigators.md"&gt;Navigator&lt;/a&gt; in &lt;a href="https://djangonaut.space/sessions/2025-session-5/"&gt;Session 5&lt;/a&gt; for 2 amazing Djangonauts, and got to hang out at &lt;a href="https://mastodon.social/@webology"&gt;Jeff&lt;/a&gt;'s Office Hours pretty consistently (though not as often as I would have liked!)&lt;/p&gt;
&lt;p&gt;The biggest accomplishment was &lt;a href="https://www.djangoproject.com/weblog/2025/nov/28/2026-dsf-board-election-results/"&gt;getting elected to the DSF Board&lt;/a&gt; and then being &lt;a href="https://www.djangoproject.com/weblog/2025/dec/18/introducing-the-2026-dsf-board/"&gt;elected Treasurer of the Board&lt;/a&gt;. This is still all &lt;strong&gt;very&lt;/strong&gt; new and I'm trying to feel my way around, but I've got some amazing support and I'm really looking forward to working with the board in 2026 and beyond.&lt;/p&gt;
&lt;h2&gt;Technology&lt;/h2&gt;
&lt;p&gt;A few weeks ago I watched Jeff Triplet migrate various infrastructure for &lt;a href="https://djangopackages.org/"&gt;DjangoPackages.org&lt;/a&gt; from &lt;a href="https://www.digitalocean.com/"&gt;Digital Ocean&lt;/a&gt; to &lt;a href="https://www.hetzner.com/"&gt;Hetzner&lt;/a&gt; with &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;. This got me to dive into that ... pretty deeply. I spent a lot of my December PTO&lt;sup id="sf-year-in-review-2025-3-back"&gt;&lt;a href="#sf-year-in-review-2025-3" class="simple-footnote" title="Paid Time Off / Vacation"&gt;3&lt;/a&gt;&lt;/sup&gt; working to migrate my servers from Digital Ocean to Hetzner managed with Coolify. I plan to write more about that later, but needless to say, as of December 29, 2025 I had successfully migrated everything off Digital Ocean to Hetzner.&lt;/p&gt;
&lt;h2&gt;Personal&lt;/h2&gt;
&lt;h3&gt;Music&lt;/h3&gt;
&lt;p&gt;Watching live music is a lot of fun. My wife Emily and I really enjoy doing this. We didn't get to see as many concerts as we would have liked to, but we were still able to see a few. &lt;a href="https://www.kelseaballerini.com/home"&gt;Kelsea Ballerini&lt;/a&gt; (with our daughter Abby) and &lt;a href="https://www.bensonboone.com/"&gt;Benson Boone&lt;/a&gt; at &lt;a href="https://en.wikipedia.org/wiki/Crypto.com_Arena"&gt;Crypto.com arena&lt;/a&gt; in Downtown Los Angeles, &lt;a href="https://sessantalive.com/?srsltid=AfmBOoqKeaTi6S9G5bzemiit74Ed6cuRHGyKm4G3gwZ1yU0Y9xVVL49H"&gt;Sessanta&lt;/a&gt; at &lt;a href="https://en.wikipedia.org/wiki/Acrisure_Arena"&gt;Acrisure Arena&lt;/a&gt;, &lt;a href="https://www.thirdeyeblind.com/"&gt;Third Eye Blind&lt;/a&gt;&lt;sup id="sf-year-in-review-2025-4-back"&gt;&lt;a href="#sf-year-in-review-2025-4" class="simple-footnote" title="yes, they are still a band ... no this was not my idea!"&gt;4&lt;/a&gt;&lt;/sup&gt; at a local casino, a show at the &lt;a href="https://en.wikipedia.org/wiki/Grand_Ole_Opry"&gt;Grand Ole Opry&lt;/a&gt; in Nashville, Tennessee as part of a conference I attended. We also saw about  5 or 6 different bands in 3 days at different venues while we were in Nashville (though sadly we didn't get to see a show on the stage at the &lt;a href="https://locations.tacobell.com/tn/nashville/131-2nd-avenue-north.html"&gt;Taco Bell Cantina&lt;/a&gt; which feels like a real miss!) though we did get to see &lt;a href="https://www.thesteeldrivers.com/"&gt;The Steel Drivers&lt;/a&gt; at the historic &lt;a href="https://www.ryman.com/"&gt;Ryman Auditorium&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We also saw &lt;a href="https://postmodernjukebox.com/"&gt;Post Modern Jukebox&lt;/a&gt; at the &lt;a href="https://www.foxriverside.com/"&gt;Fox Theater&lt;/a&gt; in &lt;a href="https://www.visitcalifornia.com/places-to-visit/riverside/"&gt;Riverside&lt;/a&gt;. We stayed a few days to get out of the heat of the Coachella Valley, but were shocked to learn that Riverside is only about 15-20 degrees cooler. And when it's 115-120 here it can still be above 100 there!&lt;/p&gt;
&lt;p&gt;We still had a great time and it convinced me even more that the wild idea my friend &lt;a href="https://pythonbynight.com/"&gt;Mario&lt;/a&gt; and I had to pitch Riverside as a location for DjangoCon US 2027/2028 was actually a really good idea, not just a wild idea 😀&lt;/p&gt;
&lt;h3&gt;Hockey&lt;/h3&gt;
&lt;p&gt;I went to a ton of hockey games. To start the year off I went to the &lt;a href="https://acrisurearena.com/event/cactus-cup-2/"&gt;Cactus Cup&lt;/a&gt; and saw 4 NCAA Division 1 games in 2 days. The best part was sitting behind the goal right at the glass and seeing just how fast (and LOUD) the game can be.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Teddy Bear Toss 2025" src="https://ryancheley.com/images/Cactus-Cup-2025.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;I was pretty exhausted (but happy) by the end of it.&lt;/p&gt;
&lt;p&gt;I also got to see 36 &lt;a href="https://cvfirebirds.com/"&gt;Firebirds&lt;/a&gt; home games (regular season&lt;sup id="sf-year-in-review-2025-5-back"&gt;&lt;a href="#sf-year-in-review-2025-5" class="simple-footnote" title="This includes games for the 2024-25 and 2025-26 season"&gt;5&lt;/a&gt;&lt;/sup&gt; and post season&lt;sup id="sf-year-in-review-2025-6-back"&gt;&lt;a href="#sf-year-in-review-2025-6" class="simple-footnote" title="just 2025, obviously"&gt;6&lt;/a&gt;&lt;/sup&gt;), 2 Firebirds road games (both in San Diego against the &lt;a href="https://www.sandiegogulls.com/"&gt;Gulls&lt;/a&gt;), and was able to attend the &lt;a href="https://theahl.com/news/coachella-valley-to-host-2025-ahl-all-star-classic"&gt;AHL All Star Competition&lt;/a&gt; at &lt;a href="https://en.wikipedia.org/wiki/Acrisure_Arena"&gt;Acrisure Arena&lt;/a&gt; in February.&lt;/p&gt;
&lt;p&gt;On the NHL front I went to a game to watch the &lt;a href="https://www.nhl.com/kings/"&gt;LA Kings&lt;/a&gt; host the &lt;a href="https://www.nhl.com/kraken/"&gt;Seattle Kraken&lt;/a&gt; (the Firebirds big kid club) and while I was in Nashville for a conference in November I got to watch the &lt;a href="https://www.nhl.com/predators/"&gt;Nashville Predators&lt;/a&gt; play the &lt;a href="https://www.nhl.com/flames/"&gt;Calgary Flames&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My favorite hockey-related experiences this year though were the &lt;a href="https://en.wikipedia.org/wiki/Teddy_bear_toss"&gt;Teddy Bear Toss&lt;/a&gt; &lt;a href="https://theahl.com/stats/game-center/1028181"&gt;game&lt;/a&gt; (even though the Firebirds lost) and getting to greet the players in the tunnel before the game&lt;/p&gt;
&lt;p&gt;&lt;img alt="Teddy Bear Toss 2025" src="https://ryancheley.com/images/Teddy-BearToss-2025.jpeg"&gt;&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/vsi4cQ1IKdw?si=Syof8OuV9vRhqOsA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""&gt;&lt;/iframe&gt;

&lt;h3&gt;Baseball&lt;/h3&gt;
&lt;p&gt;Sadly we only made it out to a few baseball games this year. We saw a few California Winter League games one Saturday in February, and one game out in Rancho Cucamungo to see the Dodgers Low A Affiliate the Quakes play. The Dodgers won the World Series, and that was nice, but I didn't see any of their games in person this year.&lt;/p&gt;
&lt;h2&gt;Family&lt;/h2&gt;
&lt;p&gt;This year Abby started her second year of College so Emily and I continue to be empty nesters most of the year&lt;sup id="sf-year-in-review-2025-7-back"&gt;&lt;a href="#sf-year-in-review-2025-7" class="simple-footnote" title="I might just be misremembering, but when I was in college we didn't have this many breaks, and they weren't this long!"&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;One of my favorite highlights from the year include Abby coming home from college for the weekend for my birthday last March to surprise me. A completely unexpected visit that literally made it one of the best birthdays ever. I read something earlier this year that once your child leaves your home for college, or whatever, they'll have spent about 95% of the time they're EVER going to spend with you. This hit me pretty hard. Like wanting to sob uncontrollably hard. So for Abby to come home to spend time with me for my birthday was the best gift ever.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Looking back it was a pretty great year. Lots of accomplishments, lots of great memories. The year started off with lots of fear and trepidation. I still have that (in spades) but I also am starting to have a bit more hope.&lt;/p&gt;
&lt;p&gt;I don't have any lofty goals, and I didn't do the same kind of &lt;a href="https://youtu.be/NVGuFdX5guE?si=FewkUjQis5kjiQTv"&gt;Theme&lt;/a&gt; planning that I've done in the past. This year it just didn't really work for me, so I'm pausing on that exercise.&lt;/p&gt;
&lt;p&gt;That being said, if I was going to have a theme for 2026 it would be 'The year of Intentionality'. I've spent more time this year than I would have liked doing things but not thinking about what I wanted to do. I just did them because they were easy or it's just what I always do. For the last few weeks I've been trying (with varying degrees of success) to be more intentional in my actions, and my plan is to continue that into 2026.&lt;/p&gt;
&lt;p&gt;Here's hoping to a great 2026!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-year-in-review-2025-1"&gt;The department is 13 people &lt;a href="#sf-year-in-review-2025-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-2"&gt;&lt;a href="https://youtu.be/LG-3TB8GIZA?si=GbHaFNdb0Y9KyMcT"&gt;DCUS 2023&lt;/a&gt;, &lt;a href="https://youtu.be/VPldDxuJDsg?si=YH9nCZxNsYX5Baj3"&gt;DCUS 2024&lt;/a&gt; &lt;a href="#sf-year-in-review-2025-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-3"&gt;Paid Time Off / Vacation &lt;a href="#sf-year-in-review-2025-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-4"&gt;yes, they are still a band ... no this was not my idea! &lt;a href="#sf-year-in-review-2025-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-5"&gt;This includes games for the 2024-25 and 2025-26 season &lt;a href="#sf-year-in-review-2025-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-6"&gt;just 2025, obviously &lt;a href="#sf-year-in-review-2025-6-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2025-7"&gt;I might just be misremembering, but when I was in college we didn't have this many breaks, and they weren't this long! &lt;a href="#sf-year-in-review-2025-7-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category></entry><entry><title>Details on My Candidate Statement for the DSF</title><link href="https://ryancheley.com/2025/11/04/dsf-candidate-statement/" rel="alternate"></link><published>2025-11-04T00:00:00-08:00</published><updated>2025-11-04T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-11-04:/2025/11/04/dsf-candidate-statement/</id><summary type="html">&lt;p&gt;The &lt;a href="https://www.djangoproject.com/weblog/2025/oct/11/2026-dsf-board-nominations/"&gt;Django Software Foundation Board of Directors elections&lt;/a&gt; are scheduled for November 2025 and I’ve decided to throw my hat into the ring. My hope specifically, if elected, is to be selected as the Treasurer. I have 4 main objectives over my two year term.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Getting an Executive Director …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;The &lt;a href="https://www.djangoproject.com/weblog/2025/oct/11/2026-dsf-board-nominations/"&gt;Django Software Foundation Board of Directors elections&lt;/a&gt; are scheduled for November 2025 and I’ve decided to throw my hat into the ring. My hope specifically, if elected, is to be selected as the Treasurer. I have 4 main objectives over my two year term.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Getting an Executive Director (ED) to help run the day-to-day operations of the DSF&lt;/li&gt;
&lt;li&gt;Identifying small to midsized companies for sponsorships&lt;/li&gt;
&lt;li&gt;Implementing a formal strategic planning process&lt;/li&gt;
&lt;li&gt;Setting up a fiscal sponsorship program to allow support of initiatives like &lt;a href="https://django-commons.org/"&gt;Django Commons&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are outlined in my &lt;a href="link-goes-here"&gt;candidate statement&lt;/a&gt;, but I want to provide a bit more detail why I think they’re important, and some high level details on a plan to get them to completion.&lt;/p&gt;
&lt;p&gt;These four goals are interconnected. We need an ED to scale operations, but funding an ED requires increased revenue through corporate sponsorships. Both benefit from having a strategic plan that guides priorities. And fiscal sponsorship potentially creates a new revenue stream while strengthening the ecosystem. This isn't four separate initiatives - it's a coherent plan for sustainable growth.&lt;/p&gt;
&lt;h2&gt;Getting an Executive Director (ED) to help run the day-to-day operations of the DSF&lt;/h2&gt;
&lt;p&gt;An ED provides day-to-day operational capacity that volunteer boards simply cannot match. While board members juggle DSF work with full-time jobs, an ED could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Call potential corporate sponsors every week, not just when someone has spare time&lt;/li&gt;
&lt;li&gt;Coordinate directly with Django Fellows on priorities and deliverables&lt;/li&gt;
&lt;li&gt;Support DjangoCon organizers across North &amp;amp; South America, Europe, Africa, and Asia with logistics and continuity&lt;/li&gt;
&lt;li&gt;Respond to the steady stream of trademark, licensing, and community inquiries&lt;/li&gt;
&lt;li&gt;Write grant applications to foundations that fund open source&lt;/li&gt;
&lt;li&gt;Prepare board materials so directors can focus on governance, not research&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As Jacob Kaplan-Moss says in his &lt;a href="https://youtu.be/5nS1SSuHk9I?si=iWUzoiHnTKtY2mKe&amp;amp;t=322"&gt;2024 DjangoCon US talk&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We’re already at the limit of what a volunteer board can accomplish&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Right now we're missing opportunities because volunteer bandwidth is maxed out. We can't pursue major corporate sponsors that need regular touchpoints. We can't support ecosystem projects that need fiscal sponsorship. We can't scale the Fellows program even though there's clearly more work than the current Fellows can handle.&lt;/p&gt;
&lt;p&gt;As Treasurer, hiring an ED would be my top priority. Based on comparable nonprofit ED salaries, a part-time ED (20 hours/week) would cost approximately $60,000-$75,000 annually including benefits and overhead. A full-time ED would be $120,000-$150,000.&lt;/p&gt;
&lt;p&gt;The DSF's current annual budget is roughly $300,000. Adding even a part-time ED would require increasing revenue by 25-30%. This is exactly why my second priority focuses on corporate sponsorships - we need sustainable revenue growth to support professional operations.&lt;/p&gt;
&lt;p&gt;The path forward is phased: board members initiate corporate outreach to fund a part-time ED, who then scales up fundraising efforts to eventually become full-time and bring us toward that $1M budget Jacob outlined. We bootstrap professional operations through volunteer effort, then let the professional accelerate what volunteers started.&lt;/p&gt;
&lt;h2&gt;Identifying small to midsized companies for sponsorships&lt;/h2&gt;
&lt;p&gt;In Jacob Kaplan-Moss' &lt;a href="https://youtu.be/5nS1SSuHk9I?si=iWUzoiHnTKtY2mKe&amp;amp;t=322"&gt;2024 DjangoCon US Talk&lt;/a&gt;, he outlines what the DSF could do with a $1M budget. I believe this is achievable, but it requires a systematic approach to corporate sponsorships.&lt;/p&gt;
&lt;p&gt;Currently, the DSF focuses primarily on &lt;a href="https://www.djangoproject.com/foundation/corporate-members/"&gt;major sponsors&lt;/a&gt;. This makes sense - volunteer boards have limited bandwidth, so targeting "whales" is efficient. But we're leaving significant revenue on the table.&lt;/p&gt;
&lt;p&gt;Consider the numbers: US Census data shows roughly 80,000-400,000 small to mid-sized tech companies (depending on definitions). Stack Overflow's 2024 survey indicates 46.9% of professional developers use Python, and 38% of Python web developers use Django. Even capturing a small fraction of companies using Django in production at a modest sponsorship tier ($500-$2,500/year) could significantly increase DSF revenue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The challenge isn't identifying companies - it's having capacity to reach them.&lt;/strong&gt; This is where an Executive Director becomes critical.&lt;/p&gt;
&lt;h3&gt;What an Executive Director Would Enable&lt;/h3&gt;
&lt;p&gt;A part-time Executive Director (ED) could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dedicate 10+ hours weekly to corporate outreach instead of the 1-2 hours volunteer board members can spare&lt;/li&gt;
&lt;li&gt;Maintain a CRM system tracking sponsor relationships, touchpoints, and renewal cycles&lt;/li&gt;
&lt;li&gt;Create targeted outreach campaigns to Django-using companies in specific sectors (healthcare tech, fintech, e-commerce, etc.)&lt;/li&gt;
&lt;li&gt;Develop case studies showing Django's business value to help companies justify sponsorship&lt;/li&gt;
&lt;li&gt;Provide consistent follow-up and relationship management that volunteer boards cannot maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;My First 90 Days as Treasurer&lt;/h3&gt;
&lt;p&gt;If elected, here's my concrete plan:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Month 1:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Audit current sponsors and revenue sources&lt;/li&gt;
&lt;li&gt;Identify 20 target companies (mix of sizes) currently using Django&lt;/li&gt;
&lt;li&gt;Work with current board to draft outreach templates and sponsorship value propositions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Month 2:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Begin systematic outreach to target companies&lt;/li&gt;
&lt;li&gt;Track response rates and refine approach&lt;/li&gt;
&lt;li&gt;Engage with Django community leaders to identify additional prospects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Month 3:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Report results to board&lt;/li&gt;
&lt;li&gt;If we've secured commitments for additional $30K-$50K in annual recurring revenue, propose budget to hire part-time ED&lt;/li&gt;
&lt;li&gt;Continue to push forward the ED recruitment process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is realistic volunteer-level effort (5-8 hours/week) that proves the concept before committing to an ED hire. Once we have an ED, they can scale this 5-10x.&lt;/p&gt;
&lt;h2&gt;Implementing a formal strategic planning process&lt;/h2&gt;
&lt;p&gt;The DSF needs a strategic plan - not as a bureaucratic exercise, but as a practical tool for making decisions and measuring progress.&lt;/p&gt;
&lt;p&gt;Right now, we operate somewhat reactively. The Fellows program exists because it was created years ago. DjangoCons happen because organizers step up. Corporate sponsorships come in when companies reach out to us. This isn't necessarily bad, but it means we're not proactively asking: What should Django's ecosystem look like in 5 years? How do we get there?&lt;/p&gt;
&lt;p&gt;A strategic plan would give us:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clear priorities:&lt;/strong&gt; When opportunities arise (a major donor, a new initiative, a partnership proposal), we can evaluate them against stated goals rather than deciding ad-hoc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Accountability:&lt;/strong&gt; We can measure whether we're making progress on what we said mattered. Did we grow the Fellows program like we planned? Did sponsorship revenue increase as projected?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Communication:&lt;/strong&gt; Community members and potential sponsors can understand where the DSF is headed and how they can contribute.&lt;/p&gt;
&lt;p&gt;As someone who's been in healthcare management since 2012, I've seen how strategic planning drives organizational effectiveness. The best plans aren't 50-page documents that sit on a shelf - they're living documents that inform quarterly board discussions and annual budget decisions.&lt;/p&gt;
&lt;p&gt;For the DSF, I envision a strategic planning process that:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Year 1:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Conduct stakeholder interviews with Fellows, corporate sponsors, community leaders, and DjangoCon organizers&lt;/li&gt;
&lt;li&gt;Identify 3-5 strategic priorities for the next 3 years (e.g., "double sponsorship revenue," "launch fiscal sponsorship program," "expand geographic diversity of Django community")&lt;/li&gt;
&lt;li&gt;Develop measurable outcomes for each priority&lt;/li&gt;
&lt;li&gt;Share draft plan with community for feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Ongoing:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Review progress quarterly at board meetings&lt;/li&gt;
&lt;li&gt;Publish annual progress reports&lt;/li&gt;
&lt;li&gt;Revise plan every 3 years based on outcomes and changing needs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This connects directly to my other goals: we need a strategic plan to guide ED hiring, fundraising priorities, and fiscal sponsorship criteria. Without it, we're making isolated decisions rather than building toward a coherent vision.&lt;/p&gt;
&lt;h2&gt;Setting up a fiscal sponsorship program to allow support of initiatives like Django Commons&lt;/h2&gt;
&lt;p&gt;Django's success isn't just about the framework itself. It's about the ecosystem of packages, tools, and community organizations that have grown around it. Projects like &lt;a href="https://django-commons.org/"&gt;Django Commons&lt;/a&gt;, &lt;a href="https://djangopackages.org/"&gt;Django Packages&lt;/a&gt;, regional Django user groups, and specialized packages serve thousands of developers daily. Yet these projects face a common challenge: they lack the legal and financial infrastructure to accept donations, pay for infrastructure, or compensate maintainers.&lt;/p&gt;
&lt;p&gt;A fiscal sponsorship program would allow the DSF to serve as the legal and financial home for vetted Django ecosystem projects. Think of it as the DSF saying: "We'll handle the paperwork, taxes, and compliance; you focus on serving the community."&lt;/p&gt;
&lt;h3&gt;Who This Helps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Community maintainers&lt;/strong&gt; who need to accept donations but shouldn't have to become nonprofit experts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Django Commons&lt;/strong&gt; and similar initiatives that need to pay for infrastructure, security audits, or maintainer stipends&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regional Django organizations&lt;/strong&gt; that want to organize events or workshops but lack financial infrastructure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critical packages&lt;/strong&gt; in the Django ecosystem that need sustainable funding models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corporate sponsors&lt;/strong&gt; who want to support the broader ecosystem but need a tax-deductible vehicle&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;Right now, valuable Django ecosystem projects are essentially flying without a net. If Django Commons needs to accept a $10,000 corporate donation to fund security audits, there's no clear path to doing so. If a critical package needs to pay for CI/CD infrastructure or compensate a maintainer for urgent security fixes, they're stuck. Some projects, such as &lt;a href="https://djangonaut.space/"&gt;Djangonaut Space&lt;/a&gt;, have tried to solve this individually by creating their own 501(c)(3)s or using platforms like Open Collective, but this fragments the community and creates overhead.&lt;/p&gt;
&lt;p&gt;The Python Software Foundation already does this successfully for &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;, &lt;a href="https://pyladies.com/"&gt;PyLadies&lt;/a&gt;, and regional Python conferences. &lt;a href="https://numfocus.org/"&gt;NumFOCUS&lt;/a&gt; sponsors &lt;a href="https://numfocus.org/sponsored-projects"&gt;dozens of scientific Python projects&lt;/a&gt;. There's no reason Django's ecosystem shouldn't have similar support.&lt;/p&gt;
&lt;p&gt;For the DSF, this is also about long-term sustainability. A healthy Django depends on a healthy ecosystem. When popular packages go unmaintained or community initiatives shut down due to funding constraints, Django suffers. By providing fiscal sponsorship, we strengthen the entire Django community while also creating a new (modest) revenue stream through administrative fees that can fund DSF operations.&lt;/p&gt;
&lt;h2&gt;Moving Forward Together&lt;/h2&gt;
&lt;p&gt;These four initiatives - (1) hiring an Executive Director, (2) growing corporate sponsorships, (3) implementing strategic planning, and (4) launching fiscal sponsorship - represent an ambitious but achievable vision for the DSF's next two years. They're not just ideas; they're a roadmap for taking Django from a volunteer-run project to a professionally-supported ecosystem that can serve millions of developers for decades to come.&lt;/p&gt;
&lt;p&gt;If you believe in this vision and think I can help make it happen, I'd be honored to have your vote. You can find more about my background and community involvement in my &lt;a href="https://www.djangoproject.com/weblog/2025/nov/05/2026-dsf-board-candidates/#ryan-cheley"&gt;candidate statement&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thank you for taking the time to read this, and regardless of the election outcome, I'm committed to supporting Django's continued success.&lt;/p&gt;</content><category term="technology"></category><category term="dsf"></category></entry><entry><title>Deploying n8n on Digital Ocean</title><link href="https://ryancheley.com/2025/10/11/deploying-n8n-on-digital-ocean/" rel="alternate"></link><published>2025-10-11T00:00:00-07:00</published><updated>2025-10-11T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-10-11:/2025/10/11/deploying-n8n-on-digital-ocean/</id><summary type="html">&lt;p&gt;This guide shows you how to deploy &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt;, a workflow automation tool, on your own VPS. Self-hosting gives you full control over your data, avoids monthly subscription costs, and lets you run unlimited workflows without usage limits.&lt;/p&gt;
&lt;p&gt;I'm using &lt;a href="https://m.do.co/c/cc5fdad15654"&gt;Digital Ocean&lt;/a&gt;&lt;sup id="sf-deploying-n8n-on-digital-ocean-1-back"&gt;&lt;a href="#sf-deploying-n8n-on-digital-ocean-1" class="simple-footnote" title="Referral Link"&gt;1&lt;/a&gt;&lt;/sup&gt; for this guide, but these steps work on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This guide shows you how to deploy &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt;, a workflow automation tool, on your own VPS. Self-hosting gives you full control over your data, avoids monthly subscription costs, and lets you run unlimited workflows without usage limits.&lt;/p&gt;
&lt;p&gt;I'm using &lt;a href="https://m.do.co/c/cc5fdad15654"&gt;Digital Ocean&lt;/a&gt;&lt;sup id="sf-deploying-n8n-on-digital-ocean-1-back"&gt;&lt;a href="#sf-deploying-n8n-on-digital-ocean-1" class="simple-footnote" title="Referral Link"&gt;1&lt;/a&gt;&lt;/sup&gt; for this guide, but these steps work on any VPS provider. You'll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A VPS with Ubuntu 24.04 (minimum 1GB RAM)&lt;/li&gt;
&lt;li&gt;A domain name with DNS access&lt;/li&gt;
&lt;li&gt;Basic familiarity with SSH and command line tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Create and configure the VPS&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.digitalocean.com/products/droplets/how-to/create/"&gt;Create a droplet&lt;/a&gt; with Ubuntu 24.04. Select a plan with at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1GB RAM&lt;/li&gt;
&lt;li&gt;25GB Disk&lt;/li&gt;
&lt;li&gt;1 vCPU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note the IP address - you'll need it for DNS configuration.&lt;/p&gt;
&lt;p&gt;SSH into the server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;ipaddress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Update the system:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt update
apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Install Docker&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository"&gt;Install Docker&lt;/a&gt; using the official repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s s-Atom"&gt;#&lt;/span&gt; &lt;span class="nv"&gt;Add&lt;/span&gt; &lt;span class="nv"&gt;Docker&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="s s-Atom"&gt;s&lt;/span&gt; &lt;span class="s s-Atom"&gt;official&lt;/span&gt; &lt;span class="nv"&gt;GPG&lt;/span&gt; &lt;span class="s s-Atom"&gt;key&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;get&lt;/span&gt; &lt;span class="s s-Atom"&gt;update&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;get&lt;/span&gt; &lt;span class="s s-Atom"&gt;install&lt;/span&gt; &lt;span class="s s-Atom"&gt;ca&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;certificates&lt;/span&gt; &lt;span class="s s-Atom"&gt;curl&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;m&lt;/span&gt; &lt;span class="mi"&gt;0755&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;keyrings&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;fsSL&lt;/span&gt; &lt;span class="s s-Atom"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="s s-Atom"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;linux&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;gpg&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;o&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;keyrings&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;asc&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;chmod&lt;/span&gt; &lt;span class="s s-Atom"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s s-Atom"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;keyrings&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;asc&lt;/span&gt;

&lt;span class="s s-Atom"&gt;#&lt;/span&gt; &lt;span class="nv"&gt;Add&lt;/span&gt; &lt;span class="s s-Atom"&gt;the&lt;/span&gt; &lt;span class="s s-Atom"&gt;repository&lt;/span&gt; &lt;span class="s s-Atom"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;Apt&lt;/span&gt; &lt;span class="s s-Atom"&gt;sources&lt;/span&gt;
&lt;span class="s s-Atom"&gt;echo&lt;/span&gt; &lt;span class="s s-Atom"&gt;\&lt;/span&gt;
  &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="s s-Atom"&gt;deb&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s s-Atom"&gt;arch=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s s-Atom"&gt;dpkg&lt;/span&gt; &lt;span class="s s-Atom"&gt;--print&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;architecture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s s-Atom"&gt;signed&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;by=/etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;keyrings&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="s s-Atom"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="s s-Atom"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;linux&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;ubuntu&lt;/span&gt; &lt;span class="s s-Atom"&gt;\&lt;/span&gt;
  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;os&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;release&lt;/span&gt; &lt;span class="s s-Atom"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s s-Atom"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"${UBUNTU_CODENAME:-$VERSION_CODENAME}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s s-Atom"&gt;stable&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s s-Atom"&gt;\&lt;/span&gt;
  &lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;tee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;list&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s s-Atom"&gt;null&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;get&lt;/span&gt; &lt;span class="s s-Atom"&gt;update&lt;/span&gt;

&lt;span class="s s-Atom"&gt;#&lt;/span&gt; &lt;span class="nv"&gt;Install&lt;/span&gt; &lt;span class="nv"&gt;Docker&lt;/span&gt; &lt;span class="s s-Atom"&gt;and&lt;/span&gt; &lt;span class="s s-Atom"&gt;its&lt;/span&gt; &lt;span class="s s-Atom"&gt;components&lt;/span&gt;
&lt;span class="s s-Atom"&gt;sudo&lt;/span&gt; &lt;span class="s s-Atom"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;get&lt;/span&gt; &lt;span class="s s-Atom"&gt;install&lt;/span&gt; &lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;ce&lt;/span&gt; &lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;ce&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;cli&lt;/span&gt; &lt;span class="s s-Atom"&gt;containerd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s s-Atom"&gt;io&lt;/span&gt; &lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;buildx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;plugin&lt;/span&gt; &lt;span class="s s-Atom"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s s-Atom"&gt;plugin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Configure DNS&lt;/h2&gt;
&lt;p&gt;Create an A record at your domain registrar pointing your subdomain (e.g., &lt;code&gt;n8n.yourdomain.com&lt;/code&gt;) to your droplet's IP address. If you're using Hover, follow &lt;a href="https://support.hover.com/support/solutions/articles/201000064728-managing-dns-records"&gt;their DNS management guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Create Docker Compose configuration&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file on your server. Start with the Caddy service for handling SSL and reverse proxy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"80:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"443:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_data:/data&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_config:/config&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./logs:/var/log/caddy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'0.5'&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;500M&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"caddy"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"json-file"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"10m"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create a &lt;code&gt;Caddyfile&lt;/code&gt; in the same directory, replacing &lt;code&gt;n8n.mydomain.com&lt;/code&gt; with your actual domain:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mydomain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Enable compression&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Reverse proxy to n8n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;reverse_proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;header_up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;header_up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Real&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;header_up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Forwarded&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;header_up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Forwarded&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;header_up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Forwarded&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;keepalive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;keepalive_idle_conns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;flush_interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Security headers (relaxed CSP for n8n's dynamic interface)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Transport&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max-age=31536000; includeSubDomains; preload"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nosniff"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SAMEORIGIN"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Referrer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strict-origin-when-cross-origin"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' wss: ws:; frame-src 'self'; worker-src 'self' blob:;"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Enable logging&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;caddy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;access&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;roll_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;MB&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;roll_keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Enable TLS with reasonable settings&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tls1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tls1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Add n8n to Docker Compose&lt;/h2&gt;
&lt;p&gt;Add the n8n service under &lt;code&gt;services:&lt;/code&gt; in your &lt;code&gt;docker-compose.yml&lt;/code&gt; file. Replace &lt;code&gt;n8n.mydomain.com&lt;/code&gt; with your domain in the environment variables:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8nio/n8n:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;N8N_HOST=n8n.mydomain.com&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;N8N_PORT=5678&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;WEBHOOK_URL=https://n8n.mydomain.com/&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;GENERIC_TIMEZONE=UTC&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"5678:5678"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'1.0'&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1G&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"wget"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--no-verbose"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--tries=1"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--spider"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"http://localhost:5678/healthz"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;60s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"json-file"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"10m"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add &lt;code&gt;n8n_data:&lt;/code&gt; to the &lt;code&gt;volumes:&lt;/code&gt; section in your &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;n8n_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# new line&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Your final &lt;code&gt;docker-compose.yml&lt;/code&gt; file will look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"80:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"443:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_data:/data&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_config:/config&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./logs:/var/log/caddy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'0.5'&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;500M&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"caddy"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"json-file"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"10m"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8nio/n8n:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;N8N_HOST=n8n.mydomain.com&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;N8N_PORT=5678&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;WEBHOOK_URL=https://n8n.mydomain.com/&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;GENERIC_TIMEZONE=UTC&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"5678:5678"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'1.0'&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1G&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"wget"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--no-verbose"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--tries=1"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--spider"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"http://localhost:5678/healthz"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;60s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"json-file"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"10m"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;n8n_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Start the containers&lt;/h2&gt;
&lt;p&gt;Run the containers in detached mode:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Complete the setup&lt;/h2&gt;
&lt;p&gt;Navigate to &lt;code&gt;https://n8n.yourdomain.com&lt;/code&gt; in your browser. Follow the setup wizard to create your admin account. Once complete, you can start building workflows.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-deploying-n8n-on-digital-ocean-1"&gt;Referral Link &lt;a href="#sf-deploying-n8n-on-digital-ocean-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="how-to"></category></entry><entry><title>Why We Need to Stop Fighting About AI Tools and Start Teaching Them</title><link href="https://ryancheley.com/2025/07/25/why-we-need-to-stop-fighting-about-ai-tools-and-start-teaching-them/" rel="alternate"></link><published>2025-07-25T00:00:00-07:00</published><updated>2025-07-25T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-07-25:/2025/07/25/why-we-need-to-stop-fighting-about-ai-tools-and-start-teaching-them/</id><summary type="html">&lt;p&gt;In mid-June, Hynek tooted on Mastodon the &lt;a href="https://mastodon.social/@hynek/114703485524249737"&gt;following toot&lt;/a&gt;:  &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Watching the frustratingly fruitless fights over the USEFULNESS of LLM-based coding helpers, I've come down to 3 points that explain why ppl seem to live in different realities:&lt;/p&gt;
&lt;p&gt;Most programmers:&lt;/p&gt;
&lt;p&gt;1) Write inconsequential remixes of trivial code that has been written …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;In mid-June, Hynek tooted on Mastodon the &lt;a href="https://mastodon.social/@hynek/114703485524249737"&gt;following toot&lt;/a&gt;:  &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Watching the frustratingly fruitless fights over the USEFULNESS of LLM-based coding helpers, I've come down to 3 points that explain why ppl seem to live in different realities:&lt;/p&gt;
&lt;p&gt;Most programmers:&lt;/p&gt;
&lt;p&gt;1) Write inconsequential remixes of trivial code that has been written many times before.&lt;/p&gt;
&lt;p&gt;2) Lack the taste for good design &amp;amp; suck at code review in general (yours truly included).&lt;/p&gt;
&lt;p&gt;3) Lack the judgement to differentiate between 1) &amp;amp; FOSS repos of nontrivial code, leading to PR slop avalanche.&lt;/p&gt;
&lt;p&gt;1/3&lt;/p&gt;
&lt;p&gt;So, if you're writing novel code &amp;amp; not another CRUD app or API wrapper, all you can see is LLMs fall on their faces.&lt;/p&gt;
&lt;p&gt;Same goes for bigger applications if you care about design. Deceivingly, if you lack 2), you won't notice that an architecture is crap b/c it doesn't look worse than your usual stuff.&lt;/p&gt;
&lt;p&gt;That means that the era of six figures for CRUD apps is coming to an end, but it also means that Claude Code et al can be very useful for certain tasks. Not every task involves splitting atoms. 2/3&lt;/p&gt;
&lt;p&gt;2/3&lt;/p&gt;
&lt;p&gt;There's also a bit of a corollary here. Given that LLMs are stochastic parrots, the inputs determine the outputs.&lt;/p&gt;
&lt;p&gt;And, without naming names, certain communities are more… rigorous… at software design than others.&lt;/p&gt;
&lt;p&gt;It follows that the quality of LLM-generated code will inevitably become a decision factor for choosing frameworks and languages and I'm not sure if I'm ready for that.&lt;/p&gt;
&lt;p&gt;3/3&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've been having a lot of success with using Claude Code recently so I've been thinking about this toot a lot lately. Simon Willison talks a lot about the things that he's been able to do because he just asks &lt;a href="https://twimlai.com/podcast/twimlai/supercharging-developer-productivity-with-chatgpt-and-claude/"&gt;OpenAI's ChatGPT while walking his dog&lt;/a&gt;. He's asking a coding agent to help him with ideas he has in languages with which he may not be familiar. However, he's a good enough programmer that he can spot anti-patterns that are being written by the agent.&lt;/p&gt;
&lt;p&gt;For me, it comes down to the helpfulness of these agentic coding tools; they can help me write boiler plate code more quickly. What it's really coming down to, for me, is that when something is trivially easy to implement, like another CRUD app or an API wrapper, those problems are solved. We don't need to keep solving them in ways that don't really help. What we need to do in order to be better programmers is figure out how to solve problems most effectively. And if that's creating a CRUD app or an API wrapper or whatever, then yeah, you're not solving any huge problem there. But if you're looking to solve something in a very unique or novel way, agentic coding tools aren't going to help you as much.&lt;/p&gt;
&lt;p&gt;I don't need to know how the internal combustion engine of my car works. I do need to know that when the check engine light comes on, I need to take it to a mechanic. And then that mechanic is going to use some device that lets them know what is wrong with the car and what needs to be done to fix it. This seems very analogous to the coding agents that we're seeing now. We don't have to keep trying to solve those problems with well-known solutions. We can and we should rely on the knowledge that is available to us and use that knowledge to solve these problems quickly. This allows us to focus on trying to solve new problems that no one has ever seen. &lt;/p&gt;
&lt;p&gt;This doesn't mean we can skip learning the fundamentals. Like blocking and tackling in football, if you can't handle the basic building blocks of programming, you're not going to succeed with complex projects. That foundational understanding remains essential.&lt;/p&gt;
&lt;p&gt;The real value of large language models and coding agents lies in how they can accelerate that learning process. Being able to ask an LLM about how a specific GitHub action works, or why you'd want to use a particular pattern, creates opportunities to understand concepts more quickly. These tools won't solve novel problems for you—that's still the core work of being a software developer. But they can eliminate the repetitive research and boilerplate implementation that used to consume so much of our time, freeing us to focus on the problems that actually require human creativity and problem-solving skills.&lt;/p&gt;
&lt;p&gt;How many software developers write in assembly anymore? Some of us maybe, but really what it comes down to is that we don't have to. We've abstracted away a lot of that particular knowledge set to a point where we don't need it anymore. We can write code in higher-level languages to help us get to solutions more quickly. If that's the case, why shouldn't we use LLMs to help us get to solutions even more quickly?&lt;/p&gt;
&lt;p&gt;I've noticed a tendency to view LLM-assisted coding as somehow less legitimate, but this misses the opportunity to help developers integrate these tools thoughtfully into their workflow. Instead of questioning the validity of using these tools, we should be focusing on how we can help people learn to use them effectively.&lt;/p&gt;
&lt;p&gt;In the same way that we helped people to learn how to use Google, we should help them to use large language models. Back in the early 2000s when Google was just starting to become a thing, knowing how to effectively use it to exclude specific terms, search for exact phrases using quotation marks, that wasn't always known by everybody. But the people who knew how to do that were able to find things more effectively.&lt;/p&gt;
&lt;p&gt;I see a parallel here. Instead of dismissing people who use these tools, we should be asking more constructive questions: How do we help them become more effective with LLMs? How do we help them use these tools to actually learn and grow as developers?&lt;/p&gt;
&lt;p&gt;Understanding the limitations of large language models is crucial to using them well, but right now we're missing that opportunity by focusing on whether people should use them at all rather than how they can use them better.&lt;/p&gt;
&lt;p&gt;We need to take a step back and re-evaluate how we use LLMs and how we encourage others to use them. The goal is getting to a point where we understand that LLMs are one more tool in our developer toolkit, regardless of whether we're working on open-source projects or commercial software. We don't need to avoid these tools. We just need to learn how to use them more effectively, and we need to do this quickly.&lt;/p&gt;</content><category term="technology"></category><category term="ai"></category><category term="llm"></category></entry><entry><title>Migrating to Raindrop.io</title><link href="https://ryancheley.com/2025/06/19/migrating-to-raindrop-io/" rel="alternate"></link><published>2025-06-19T00:00:00-07:00</published><updated>2025-06-19T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-06-19:/2025/06/19/migrating-to-raindrop-io/</id><summary type="html">&lt;p&gt;With the announced &lt;a href="https://support.mozilla.org/en-US/kb/future-of-pocket"&gt;demise of Pocket by Mozilla&lt;/a&gt; I needed to migrate all of my saved articles to 'something else' by the end of the month. I've actually tried to migrate from Pocket a few times over the years. I landed on &lt;a href="https://www.instapaper.com/"&gt;Instapaper&lt;/a&gt; for a while, but it never really …&lt;/p&gt;</summary><content type="html">&lt;p&gt;With the announced &lt;a href="https://support.mozilla.org/en-US/kb/future-of-pocket"&gt;demise of Pocket by Mozilla&lt;/a&gt; I needed to migrate all of my saved articles to 'something else' by the end of the month. I've actually tried to migrate from Pocket a few times over the years. I landed on &lt;a href="https://www.instapaper.com/"&gt;Instapaper&lt;/a&gt; for a while, but it never really clicked for me. I tried a service called &lt;a href="https://devmarks.io/"&gt;Devmarks&lt;/a&gt; that &lt;a href="https://indieweb.social/@adamghill"&gt;Adam G Hill&lt;/a&gt; runs, and I really liked it, but for whatever reason I stopped using it. I had also previously tried &lt;a href="https://Raindrop.io/"&gt;Raindrop.io&lt;/a&gt; ... and I'm not really sure what drove me away from it, but it didn't stick for me at the time. &lt;/p&gt;
&lt;p&gt;Since I didn't have a choice about Pocket I did a bit of purusing my options, and finally landed on Raindrop.io again. The process of migration is pretty painless. I just export out the links from Pocket and then import them into Raindrop. No fuss ... no muss. Raindrop even checks for duplicates and allows you to not import them! &lt;/p&gt;
&lt;p&gt;So, I imported everything (all 11,500+ articles!) and started to incorporate Raindrop into my workflow. This basically just means saving things to Raindrop instead of pocket, and then checking Raindrop instead of Pocket every week to make sure I'm all caught up on my articles to read. &lt;/p&gt;
&lt;p&gt;Over the last weekend I was looking at how all of the imported items in Raindrop were put into the 'archive' collection and decided that I could probably do something about putting them into proper collections. &lt;/p&gt;
&lt;p&gt;With the help of Claude Code, I was able to put them into better collections. There were some stragglers and I decided that I could categorize them on my own (there were less than 100).&lt;/p&gt;
&lt;p&gt;I started going through these last ones I kept coming across articles for iOS7, or an app that I think I liked in 2015 but isn't on the App store anymore. I came across &lt;a href="https://www.inc.com/graham-winfrey/what-the-internet-of-things-will-look-like-in-2025.html"&gt;this article&lt;/a&gt; (which I also &lt;a href="https://mastodon.social/@ryancheley/114689388596371458"&gt;tooted&lt;/a&gt; about on Mastadon) from September 4, 2014 with the title &lt;code&gt;What the Internet of Things Will Look Like in 2025 (Infographic)&lt;/code&gt;. It's wildly naive, but a fun read nonetheless.&lt;/p&gt;
&lt;p&gt;Needless to say it was the only gem in the 100 articles that I went through. I had so many saved articles that aren't 'Evergreen'. I then started looking at some of the articles that had been categorized and came across stuff for Django 1.11, Python 3.8, and other older stuff. &lt;/p&gt;
&lt;p&gt;These were great articles when I read them, but I don't know that I &lt;strong&gt;need&lt;/strong&gt; them now. In fact, when I looked at my general workflow for using any read-it-later service, I essentially save it to read later. If it's sitting in my read-it-later service for more than 4 weeks I'll either delete or just archive it. &lt;/p&gt;
&lt;p&gt;So really, unless I'm planning on &lt;em&gt;doing&lt;/em&gt; something with these articles, I'm not sure that I need to keep them. And that's when it hit me ... I can just delete them. All of them. I don't need to keep them. If they are truly impactful, I can write up something about them in Obsidian. If I really think someone else will get something out of my reaction I can write it up and post it. But, if I'm being honest with myself, this is just digital clutter that isn't "sparking" any joy for me.&lt;/p&gt;
&lt;p&gt;So, just like that, I went from having 11,000+ links to having 0. And I have &lt;a href="https://inkcredibletattoovb.com/bad-tattoos-no-ragerts/"&gt;no ragrets&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;I'm sure there's some deeper story here about physical things and just letting them go as well, and maybe I'll be able to apply that to my non-digital life, but for now, I'm just going to revel in the fact that I was able to offload this thing and just not ... care? Be sad? I'm not sure what the correct term would be here. &lt;/p&gt;
&lt;p&gt;Regardless, it was a good exercise to have gone through, and I'm glad I did. &lt;/p&gt;</content><category term="musings"></category><category term="reading"></category></entry><entry><title>A New Project at Work</title><link href="https://ryancheley.com/2025/06/15/a-new-project-at-work/" rel="alternate"></link><published>2025-06-15T00:00:00-07:00</published><updated>2025-06-15T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-06-15:/2025/06/15/a-new-project-at-work/</id><summary type="html">&lt;p&gt;I was added to a work email that was requesting a not-so-small new project that was going to need to be completed. The problem that needed to be solved was a bit squishy, but it had been well thought out, and it had an importance to it that was easy …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I was added to a work email that was requesting a not-so-small new project that was going to need to be completed. The problem that needed to be solved was a bit squishy, but it had been well thought out, and it had an importance to it that was easy to see.&lt;/p&gt;
&lt;p&gt;There was still some workflows and data that needed to be reviewed, but overall it was on a good path to having a &lt;code&gt;real project&lt;/code&gt; feel to it.&lt;/p&gt;
&lt;p&gt;One question still outstanding is, what platform will this project be implemented on? In our EHR, or on a separate web app?&lt;/p&gt;
&lt;p&gt;During my weekly project review meeting with the Web Development team I let them know about the potential for this new project and that it would likely need to take priority over one of our current projects. The start is still a couple of weeks away so we have time to plan for it (as much as we can anyway). We looked at the project board and determined a ranking of the current projects. We decided on the project that would likely get bumped if this new one ends up with the web developers. And just like that we had a contingency plan for how to plan for this project given our current constraints.&lt;/p&gt;
&lt;p&gt;Now, this project may never make its way to the web development team, but having that conversation with the manager, and then during our standup today, to let the team know that this &lt;strong&gt;might&lt;/strong&gt; be something that will need to be worked on by them felt right. No surprises in a few weeks. No randomness about what projects we'll be working on ... just a bit of planning to prepare for something that might never come.&lt;/p&gt;
&lt;p&gt;Eisenhower said, "Plans are nothing, planning is everything."&lt;/p&gt;
&lt;p&gt;The team appreciated being in the loop about a potential project and being able to align expectations moving forward. I felt grateful that this was brought to my attention well before it was submitted as a request. The requester now has a bit more information on who to speak with internally, and it really felt like we were working together to solve a problem in a very professional way.&lt;/p&gt;
&lt;p&gt;I wish all projects started like this. It would make life way easier and not so much like &lt;a href="https://oldbytes.space/@tschak/114661574560412783"&gt;this&lt;/a&gt;&lt;/p&gt;</content><category term="musings"></category></entry><entry><title>Updated TIL</title><link href="https://ryancheley.com/2025/06/05/updated-til/" rel="alternate"></link><published>2025-06-05T00:00:00-07:00</published><updated>2025-06-05T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-06-05:/2025/06/05/updated-til/</id><summary type="html">&lt;p&gt;While browsing Mastodon the other day I came across a toot that linked to this &lt;a href="https://immich.app/cursed-knowledge/"&gt;Cursed Knowledge&lt;/a&gt; page. I thought it was a great page, but it occurred to me that it could be helpful to apply the same sort of styling to my TIL Repo.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/ryancheley/til"&gt;TIL&lt;/a&gt; (Today I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;While browsing Mastodon the other day I came across a toot that linked to this &lt;a href="https://immich.app/cursed-knowledge/"&gt;Cursed Knowledge&lt;/a&gt; page. I thought it was a great page, but it occurred to me that it could be helpful to apply the same sort of styling to my TIL Repo.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/ryancheley/til"&gt;TIL&lt;/a&gt; (Today I Learned) repository was inspired by &lt;a href="https://github.com/simonw/til"&gt;Simon Wilison's&lt;/a&gt; and is &lt;del&gt;basically a rip off&lt;/del&gt; completely inspired by him.&lt;/p&gt;
&lt;p&gt;Now, I'm not a front end guy, and while I can do some pretty basic stuff with GitHub Actions, I knew that what I wanted to do far outstripped my current capabilities. Enter Claude!&lt;/p&gt;
&lt;p&gt;I started with this prompt&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are an expert in GitHub Actions, and GitHub. Additionally, you are an expert in HTML, CSS, and JavaScript. The solutions provided can not leverage JavaScript frameworks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I then made this request&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I currently have a Today I learned (TIL) repository on GitHub located here&lt;/p&gt;
&lt;p&gt;https://github.com/ryancheley/til&lt;/p&gt;
&lt;p&gt;I came across the idea of documenting cursed information here&lt;/p&gt;
&lt;p&gt;https://immich.app/cursed-knowledge/&lt;/p&gt;
&lt;p&gt;I would like to implement my TIL in a timeline the same way the cursed knowledge page was done.&lt;/p&gt;
&lt;p&gt;How can I do that?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It output this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build TIL Timeline&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Rebuild daily at 6 AM UTC&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&amp;#39;&lt;/span&gt;

&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;read&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;id-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;build-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Checkout repository&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v4&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Node.js&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-node@v4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;node-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;18&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline data&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;node &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;const fs = require(&amp;#39;fs&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;const path = require(&amp;#39;path&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Function to extract metadata from markdown files&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;function extractTilData(filePath, content) {&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const relativePath = path.relative(&amp;#39;.&amp;#39;, filePath);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const fileName = path.basename(filePath, &amp;#39;.md&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;// Extract title from first heading or use filename&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const titleMatch = content.match(/^#\s+(.+)$/m);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const title = titleMatch ? titleMatch[1] : fileName.replace(/[-_]/g, &amp;#39; &amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;// Extract first paragraph as description&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const lines = content.split(&amp;#39;\n&amp;#39;).filter(line =&amp;gt; line.trim());&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;let description = &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;for (const line of lines) {&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if (!line.startsWith(&amp;#39;#&amp;#39;) &amp;amp;&amp;amp; line.trim() &amp;amp;&amp;amp; !line.startsWith(&amp;#39;```&amp;#39;)) {&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="no"&gt;description = line.trim();&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="no"&gt;break;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;// Get file stats for date&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const stats = fs.statSync(filePath);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const date = stats.mtime; // Use modification time&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;// Extract category from directory structure&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const pathParts = relativePath.split(&amp;#39;/&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const category = pathParts.length &amp;gt; 1 ? pathParts[0] : &amp;#39;General&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;return {&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;title,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;description: description || &amp;#39;No description available&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;date: date.toISOString().split(&amp;#39;T&amp;#39;)[0], // YYYY-MM-DD format&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;url: `https://github.com/ryancheley/til/blob/main/${relativePath}`,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;category,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;fileName: relativePath&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Recursively find all markdown files&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;function findMarkdownFiles(dir) {&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const files = [];&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;const entries = fs.readdirSync(dir, { withFileTypes: true });&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;for (const entry of entries) {&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;const fullPath = path.join(dir, entry.name);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if (entry.isDirectory() &amp;amp;&amp;amp; !entry.name.startsWith(&amp;#39;.&amp;#39;)) {&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="no"&gt;files.push(...findMarkdownFiles(fullPath));&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;} else if (entry.isFile() &amp;amp;&amp;amp; entry.name.endsWith(&amp;#39;.md&amp;#39;) &amp;amp;&amp;amp; entry.name !== &amp;#39;README.md&amp;#39;) {&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="no"&gt;files.push(fullPath);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;return files;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Generate timeline data&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;const markdownFiles = findMarkdownFiles(&amp;#39;.&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;const tilEntries = [];&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for (const filePath of markdownFiles) {&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;try {&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;const content = fs.readFileSync(filePath, &amp;#39;utf8&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;const tilData = extractTilData(filePath, content);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;tilEntries.push(tilData);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;} catch (error) {&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;console.error(`Error processing ${filePath}:`, error.message);&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Sort by date (newest first)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;tilEntries.sort((a, b) =&amp;gt; new Date(b.date) - new Date(a.date));&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Create output directory&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;if (!fs.existsSync(&amp;#39;docs&amp;#39;)) {&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;fs.mkdirSync(&amp;#39;docs&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;// Write timeline data&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;fs.writeFileSync(&amp;#39;docs/timeline-data.json&amp;#39;, JSON.stringify(tilEntries, null, 2));&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;console.log(`Generated timeline with ${tilEntries.length} entries`);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline HTML&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;mkdir -p docs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;cat &amp;gt; docs/index.html &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;title&amp;gt;Ryan&amp;#39;s Today I Learned Timeline&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;* {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-sizing: border-box;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;body {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-family: -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, system-ui, sans-serif;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;min-height: 100vh;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #333;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 1200px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header p {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;opacity: 0.9;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 600px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;bottom: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 2px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(to bottom, #4CAF50, #2196F3, #FF9800, #E91E63);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-left: 4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 8px 25px rgba(0,0,0,0.1);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: transform 0.3s ease, box-shadow 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transform: translateY(-5px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 15px 35px rgba(0,0,0,0.15);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: -3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;height: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #4CAF50;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 3px solid white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 50%;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:nth-child(4n+2)::before { background: #2196F3; box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.3); }&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:nth-child(4n+3)::before { background: #FF9800; box-shadow: 0 0 0 3px rgba(255, 152, 0, 0.3); }&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:nth-child(4n+4)::before { background: #E91E63; box-shadow: 0 0 0 3px rgba(233, 30, 99, 0.3); }&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: flex-start;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-wrap: wrap;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #2c3e50;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-decoration: none;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-grow: 1;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: color 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #3498db;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-shrink: 0;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-date {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea, #764ba2);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.5rem 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 20px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-category {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8f9fa;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #6c757d;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.4rem 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 15px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid #e9ecef;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-description {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #5a6c7d;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;line-height: 1.6;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.loading {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.error {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8d7da;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #721c24;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 8px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid #f5c6cb;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: rgba(255,255,255,0.95);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;backdrop-filter: blur(10px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats-grid {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: grid;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-number {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: bold;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #667eea;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: block;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-label {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;@media (max-width: 768px) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;margin-left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: -2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;flex-direction: column;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;align-items: stretch;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;h1&amp;gt;Today I Learned&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;p&amp;gt;A timeline of discoveries, learnings, and insights from my development journey&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats-grid&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;totalEntries&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Total Entries&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;categoriesCount&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Categories&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;latestEntry&amp;quot;&amp;gt;-&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Latest Entry&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;loading&amp;quot; id=&amp;quot;loading&amp;quot;&amp;gt;Loading timeline...&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;error&amp;quot; id=&amp;quot;error&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline&amp;quot; id=&amp;quot;timeline&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;async function loadTimeline() {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;try {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const response = await fetch(&amp;#39;timeline-data.json&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;if (!response.ok) {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;throw new Error(&amp;#39;Failed to load timeline data&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const entries = await response.json();&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayTimeline(entries);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayStats(entries);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;} catch (error) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;console.error(&amp;#39;Error loading timeline:&amp;#39;, error);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;loading&amp;#39;).style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).textContent = &amp;#39;Failed to load timeline. Please try again later.&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayStats(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const categories = [...new Set(entries.map(entry =&amp;gt; entry.category))];&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const latest = entries.length &amp;gt; 0 ? new Date(entries[0].date).toLocaleDateString() : &amp;#39;-&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;totalEntries&amp;#39;).textContent = entries.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;categoriesCount&amp;#39;).textContent = categories.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;latestEntry&amp;#39;).textContent = latest;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;stats&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayTimeline(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineContainer = document.getElementById(&amp;#39;timeline&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const loading = document.getElementById(&amp;#39;loading&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;loading.style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;if (entries.length === 0) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = &amp;#39;&amp;lt;p style=&amp;quot;text-align: center; color: white;&amp;quot;&amp;gt;No entries found.&amp;lt;/p&amp;gt;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineHTML = entries.map(entry =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const date = new Date(entry.date).toLocaleDateString(&amp;#39;en-US&amp;#39;, {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;year: &amp;#39;numeric&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;month: &amp;#39;short&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;day: &amp;#39;numeric&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return `&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;a href=&amp;quot;${entry.url}&amp;quot; class=&amp;quot;timeline-title&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;${entry.title}&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-meta&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-category&amp;quot;&amp;gt;${entry.category}&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-date&amp;quot;&amp;gt;${date}&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-description&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;${entry.description}&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;`;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}).join(&amp;#39;&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = timelineHTML;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;// Load timeline on page load&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;loadTimeline();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/configure-pages@v4&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Upload artifact&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/upload-pages-artifact@v3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;docs&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github-pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;build-timeline&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Deploy to GitHub Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;deployment&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/deploy-pages@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I added it to my TIL repo and tested to see how it looked. It was a great start!&lt;/p&gt;
&lt;p&gt;I then followed up with&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;this is a great start! however, it looks like the are a few issues:
1. All of the dates on the timeline are the published date and not the date of the actual learning. There is a SQLite database that stores this kind of meta data
2. The order of the items matches the order they are displayed on the Read Me, which is by category and not by date (see point 1)
3. the colors for each TIL seem to just rotate in order and aren't associated with any specific category of TIL&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude went to work and generated a new GitHub Action for me&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build TIL Timeline&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Rebuild daily at 6 AM UTC&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&amp;#39;&lt;/span&gt;

&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;read&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;id-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;build-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Check out repo&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;fetch-depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Need full history for git analysis&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v5&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3.12&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/cache@v4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Configure pip caching&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;~/.cache/pip&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ runner.os }}-pip-${{ hashFiles(&amp;#39;**/requirements.txt&amp;#39;) }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;restore-keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;${{ runner.os }}-pip-&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Python dependencies&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;python -m pip install --upgrade pip&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;pip install -r requirements.txt&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build database&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;python build_database.py&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline data from SQLite&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;python &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import sqlite3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import json&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import os&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;from pathlib import Path&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Connect to the SQLite database&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;db_path = Path(&amp;quot;tils.db&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;if not db_path.exists():&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;print(&amp;quot;Database not found!&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;exit(1)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn = sqlite3.connect(db_path)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn.row_factory = sqlite3.Row  # Enable dict-like access to rows&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Query all TIL entries, ordered by created date (actual learning date)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;cursor = conn.execute(&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;path,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;slug,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;topic,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;title,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;url,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;body,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;created,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;created_utc,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;updated,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;updated_utc&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;FROM til&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;ORDER BY created_utc DESC&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;quot;&amp;quot;&amp;quot;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;entries = []&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for row in cursor:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Extract first paragraph as description, excluding headers and code blocks&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;lines = row[&amp;#39;body&amp;#39;].split(&amp;#39;\n&amp;#39;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;description = &amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;for line in lines:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;line = line.strip()&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;if line and not line.startswith(&amp;#39;#&amp;#39;) and not line.startswith(&amp;#39;```&amp;#39;) and line != &amp;#39;---&amp;#39;:&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;description = line&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;break&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Clean up the description&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if len(description) &amp;gt; 200:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;description = description[:200] + &amp;#39;...&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Extract date from created_utc (YYYY-MM-DD format)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;created_date = row[&amp;#39;created_utc&amp;#39;].split(&amp;#39;T&amp;#39;)[0] if row[&amp;#39;created_utc&amp;#39;] else row[&amp;#39;created&amp;#39;].split(&amp;#39;T&amp;#39;)[0]&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;entry = {&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;title&amp;#39;: row[&amp;#39;title&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;description&amp;#39;: description or &amp;#39;No description available&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;date&amp;#39;: created_date,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;url&amp;#39;: row[&amp;#39;url&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;category&amp;#39;: row[&amp;#39;topic&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;fileName&amp;#39;: row[&amp;#39;path&amp;#39;].replace(&amp;#39;_&amp;#39;, &amp;#39;/&amp;#39;),  # Convert back from slug format&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;slug&amp;#39;: row[&amp;#39;slug&amp;#39;]&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;entries.append(entry)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn.close()&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Create output directory&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;os.makedirs(&amp;#39;docs&amp;#39;, exist_ok=True)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Write timeline data&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;with open(&amp;#39;docs/timeline-data.json&amp;#39;, &amp;#39;w&amp;#39;) as f:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;json.dump(entries, f, indent=2)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;print(f&amp;quot;Generated timeline with {len(entries)} entries&amp;quot;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Generate category statistics for consistent coloring&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;categories = {}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for entry in entries:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;cat = entry[&amp;#39;category&amp;#39;]&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if cat not in categories:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;categories[cat] = len([e for e in entries if e[&amp;#39;category&amp;#39;] == cat])&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Sort categories by count (most popular first) for consistent color assignment&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;sorted_categories = sorted(categories.items(), key=lambda x: x[1], reverse=True)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;category_colors = {}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Define a set of distinct colors for categories&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;color_palette = [&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#4CAF50&amp;#39;,  # Green&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#2196F3&amp;#39;,  # Blue&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FF9800&amp;#39;,  # Orange&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#E91E63&amp;#39;,  # Pink&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#9C27B0&amp;#39;,  # Purple&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#00BCD4&amp;#39;,  # Cyan&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FF5722&amp;#39;,  # Deep Orange&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#795548&amp;#39;,  # Brown&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#607D8B&amp;#39;,  # Blue Grey&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FFC107&amp;#39;,  # Amber&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#8BC34A&amp;#39;,  # Light Green&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#3F51B5&amp;#39;,  # Indigo&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#F44336&amp;#39;,  # Red&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#009688&amp;#39;,  # Teal&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#CDDC39&amp;#39;,  # Lime&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for i, (category, count) in enumerate(sorted_categories):&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;category_colors[category] = color_palette[i % len(color_palette)]&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Write category color mapping&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;with open(&amp;#39;docs/category-colors.json&amp;#39;, &amp;#39;w&amp;#39;) as f:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;json.dump(category_colors, f, indent=2)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;print(f&amp;quot;Generated color mapping for {len(category_colors)} categories&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline HTML&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;cat &amp;gt; docs/index.html &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;title&amp;gt;Ryan&amp;#39;s Today I Learned Timeline&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta name=&amp;quot;description&amp;quot; content=&amp;quot;A chronological timeline of learning discoveries from software development, featuring insights on Python, Django, SQL, and more.&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;* {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-sizing: border-box;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;body {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-family: -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, system-ui, sans-serif;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;min-height: 100vh;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #333;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 1200px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header p {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;opacity: 0.9;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 600px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filters {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: rgba(255,255,255,0.95);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;backdrop-filter: blur(10px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filter-group {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-wrap: wrap;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: center;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filter-label {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-right: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.4rem 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 20px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 2px solid transparent;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8f9fa;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;cursor: pointer;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: all 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;user-select: none;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transform: translateY(-2px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 4px 8px rgba(0,0,0,0.1);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter.active {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-color: currentColor;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;bottom: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 2px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(to bottom, #4CAF50, #2196F3, #FF9800, #E91E63);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-left: 4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 8px 25px rgba(0,0,0,0.1);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: all 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;opacity: 1;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item.hidden {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: none;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transform: translateY(-5px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 15px 35px rgba(0,0,0,0.15);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: -3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;height: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: var(--category-color, #4CAF50);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 3px solid white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 50%;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: flex-start;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-wrap: wrap;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #2c3e50;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-decoration: none;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-grow: 1;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: color 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #3498db;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-shrink: 0;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-date {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea, #764ba2);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.5rem 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 20px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-category {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: var(--category-color, #f8f9fa);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.4rem 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 15px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-description {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #5a6c7d;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;line-height: 1.6;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.loading {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.error {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8d7da;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #721c24;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 8px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid #f5c6cb;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: rgba(255,255,255,0.95);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;backdrop-filter: blur(10px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats-grid {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: grid;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-number {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: bold;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #667eea;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: block;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-label {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;@media (max-width: 768px) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;margin-left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: -2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;flex-direction: column;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;align-items: stretch;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.filter-group {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;flex-direction: column;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;align-items: stretch;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.category-filter {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;h1&amp;gt;Today I Learned&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;p&amp;gt;A chronological timeline of discoveries, learnings, and insights from my development journey&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats-grid&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;totalEntries&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Total Entries&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;categoriesCount&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Categories&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;latestEntry&amp;quot;&amp;gt;-&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Latest Entry&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;filteredCount&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Showing&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;filters&amp;quot; id=&amp;quot;filters&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;filter-group&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;filter-label&amp;quot;&amp;gt;Filter by category:&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div id=&amp;quot;categoryFilters&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;loading&amp;quot; id=&amp;quot;loading&amp;quot;&amp;gt;Loading timeline...&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;error&amp;quot; id=&amp;quot;error&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline&amp;quot; id=&amp;quot;timeline&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let allEntries = [];&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let categoryColors = {};&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let activeCategory = null;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;async function loadTimeline() {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;try {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;// Load timeline data and category colors&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const [entriesResponse, colorsResponse] = await Promise.all([&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;fetch(&amp;#39;timeline-data.json&amp;#39;),&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;fetch(&amp;#39;category-colors.json&amp;#39;)&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;]);&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;if (!entriesResponse.ok || !colorsResponse.ok) {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;throw new Error(&amp;#39;Failed to load timeline data&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries = await entriesResponse.json();&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;categoryColors = await colorsResponse.json();&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayTimeline(allEntries);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayStats(allEntries);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;createCategoryFilters();&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;} catch (error) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;console.error(&amp;#39;Error loading timeline:&amp;#39;, error);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;loading&amp;#39;).style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).textContent = &amp;#39;Failed to load timeline. Please try again later.&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function createCategoryFilters() {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const categories = [...new Set(allEntries.map(entry =&amp;gt; entry.category))];&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const filtersContainer = document.getElementById(&amp;#39;categoryFilters&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Add &amp;quot;All&amp;quot; filter&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const allFilter = document.createElement(&amp;#39;span&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.className = &amp;#39;category-filter active&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.textContent = &amp;#39;All&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.onclick = () =&amp;gt; filterByCategory(null);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;filtersContainer.appendChild(allFilter);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Add category filters&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;categories.sort().forEach(category =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const filter = document.createElement(&amp;#39;span&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.className = &amp;#39;category-filter&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.textContent = category;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.style.setProperty(&amp;#39;--category-color&amp;#39;, categoryColors[category] || &amp;#39;#666&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.onclick = () =&amp;gt; filterByCategory(category);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filtersContainer.appendChild(filter);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filters&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function filterByCategory(category) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;activeCategory = category;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Update filter button states&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.querySelectorAll(&amp;#39;.category-filter&amp;#39;).forEach(filter =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.classList.remove(&amp;#39;active&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;if ((category === null &amp;amp;&amp;amp; filter.textContent === &amp;#39;All&amp;#39;) ||&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;filter.textContent === category) {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;filter.classList.add(&amp;#39;active&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;if (category !== null) {&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;filter.style.background = categoryColors[category];&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Filter timeline items&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const filteredEntries = category ?&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries.filter(entry =&amp;gt; entry.category === category) :&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;displayTimeline(filteredEntries);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;updateFilteredCount(filteredEntries.length);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function updateFilteredCount(count) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filteredCount&amp;#39;).textContent = count;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayStats(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const categories = [...new Set(entries.map(entry =&amp;gt; entry.category))];&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const latest = entries.length &amp;gt; 0 ? new Date(entries[0].date).toLocaleDateString() : &amp;#39;-&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;totalEntries&amp;#39;).textContent = entries.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;categoriesCount&amp;#39;).textContent = categories.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;latestEntry&amp;#39;).textContent = latest;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filteredCount&amp;#39;).textContent = entries.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;stats&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayTimeline(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineContainer = document.getElementById(&amp;#39;timeline&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const loading = document.getElementById(&amp;#39;loading&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;loading.style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;if (entries.length === 0) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = &amp;#39;&amp;lt;p style=&amp;quot;text-align: center; color: white;&amp;quot;&amp;gt;No entries found.&amp;lt;/p&amp;gt;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineHTML = entries.map(entry =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const date = new Date(entry.date).toLocaleDateString(&amp;#39;en-US&amp;#39;, {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;year: &amp;#39;numeric&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;month: &amp;#39;short&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;day: &amp;#39;numeric&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const categoryColor = categoryColors[entry.category] || &amp;#39;#666&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return `&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-item&amp;quot; style=&amp;quot;--category-color: ${categoryColor}&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;a href=&amp;quot;${entry.url}&amp;quot; class=&amp;quot;timeline-title&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;${entry.title}&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-meta&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-category&amp;quot; style=&amp;quot;--category-color: ${categoryColor}; background: ${categoryColor};&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                            &lt;/span&gt;&lt;span class="no"&gt;${entry.category}&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-date&amp;quot;&amp;gt;${date}&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-description&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;${entry.description}&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;`;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}).join(&amp;#39;&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = timelineHTML;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;// Load timeline on page load&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;loadTimeline();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/configure-pages@v4&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Upload artifact&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/upload-pages-artifact@v3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;docs&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github-pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;build-timeline&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Deploy to GitHub Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;deployment&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/deploy-pages@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Another huge step forward! However, there was one last change that was needed&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;oh wow! much better! The last issue I'm seeing is that because I have so many categories, the Filter by category needs a way to wrap based on the page width. Right now it's on a single line, but would likely benefit from a css style that will wrap the lines&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And I got this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build TIL Timeline&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Rebuild daily at 6 AM UTC&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&amp;#39;&lt;/span&gt;

&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;read&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;id-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;write&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;build-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Check out repo&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;fetch-depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Need full history for git analysis&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v5&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3.12&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/cache@v4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Configure pip caching&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;~/.cache/pip&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ runner.os }}-pip-${{ hashFiles(&amp;#39;**/requirements.txt&amp;#39;) }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;restore-keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;${{ runner.os }}-pip-&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Python dependencies&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;python -m pip install --upgrade pip&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;pip install -r requirements.txt&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build database&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;python build_database.py&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline data from SQLite&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;python &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import sqlite3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import json&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;import os&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;from pathlib import Path&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Connect to the SQLite database&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;db_path = Path(&amp;quot;tils.db&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;if not db_path.exists():&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;print(&amp;quot;Database not found!&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;exit(1)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn = sqlite3.connect(db_path)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn.row_factory = sqlite3.Row  # Enable dict-like access to rows&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Query all TIL entries, ordered by created date (actual learning date)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;cursor = conn.execute(&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;path,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;slug,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;topic,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;title,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;url,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;body,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;created,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;created_utc,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;updated,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;updated_utc&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;FROM til&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;ORDER BY created_utc DESC&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;quot;&amp;quot;&amp;quot;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;entries = []&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for row in cursor:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Extract first paragraph as description, excluding headers and code blocks&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;lines = row[&amp;#39;body&amp;#39;].split(&amp;#39;\n&amp;#39;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;description = &amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;for line in lines:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;line = line.strip()&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;if line and not line.startswith(&amp;#39;#&amp;#39;) and not line.startswith(&amp;#39;```&amp;#39;) and line != &amp;#39;---&amp;#39;:&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;description = line&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;break&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Clean up the description&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if len(description) &amp;gt; 200:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;description = description[:200] + &amp;#39;...&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;# Extract date from created_utc (YYYY-MM-DD format)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;created_date = row[&amp;#39;created_utc&amp;#39;].split(&amp;#39;T&amp;#39;)[0] if row[&amp;#39;created_utc&amp;#39;] else row[&amp;#39;created&amp;#39;].split(&amp;#39;T&amp;#39;)[0]&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;entry = {&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;title&amp;#39;: row[&amp;#39;title&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;description&amp;#39;: description or &amp;#39;No description available&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;date&amp;#39;: created_date,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;url&amp;#39;: row[&amp;#39;url&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;category&amp;#39;: row[&amp;#39;topic&amp;#39;],&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;fileName&amp;#39;: row[&amp;#39;path&amp;#39;].replace(&amp;#39;_&amp;#39;, &amp;#39;/&amp;#39;),  # Convert back from slug format&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;slug&amp;#39;: row[&amp;#39;slug&amp;#39;]&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;entries.append(entry)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;conn.close()&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Create output directory&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;os.makedirs(&amp;#39;docs&amp;#39;, exist_ok=True)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Write timeline data&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;with open(&amp;#39;docs/timeline-data.json&amp;#39;, &amp;#39;w&amp;#39;) as f:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;json.dump(entries, f, indent=2)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;print(f&amp;quot;Generated timeline with {len(entries)} entries&amp;quot;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Generate category statistics for consistent coloring&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;categories = {}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for entry in entries:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;cat = entry[&amp;#39;category&amp;#39;]&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;if cat not in categories:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;categories[cat] = len([e for e in entries if e[&amp;#39;category&amp;#39;] == cat])&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Sort categories by count (most popular first) for consistent color assignment&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;sorted_categories = sorted(categories.items(), key=lambda x: x[1], reverse=True)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;category_colors = {}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Define a set of distinct colors for categories&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;color_palette = [&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#4CAF50&amp;#39;,  # Green&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#2196F3&amp;#39;,  # Blue&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FF9800&amp;#39;,  # Orange&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#E91E63&amp;#39;,  # Pink&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#9C27B0&amp;#39;,  # Purple&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#00BCD4&amp;#39;,  # Cyan&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FF5722&amp;#39;,  # Deep Orange&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#795548&amp;#39;,  # Brown&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#607D8B&amp;#39;,  # Blue Grey&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#FFC107&amp;#39;,  # Amber&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#8BC34A&amp;#39;,  # Light Green&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#3F51B5&amp;#39;,  # Indigo&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#F44336&amp;#39;,  # Red&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#009688&amp;#39;,  # Teal&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;#39;#CDDC39&amp;#39;,  # Lime&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;for i, (category, count) in enumerate(sorted_categories):&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;category_colors[category] = color_palette[i % len(color_palette)]&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Write category color mapping&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;with open(&amp;#39;docs/category-colors.json&amp;#39;, &amp;#39;w&amp;#39;) as f:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;json.dump(category_colors, f, indent=2)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;print(f&amp;quot;Generated color mapping for {len(category_colors)} categories&amp;quot;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate timeline HTML&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;cat &amp;gt; docs/index.html &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;title&amp;gt;Ryan&amp;#39;s Today I Learned Timeline&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;meta name=&amp;quot;description&amp;quot; content=&amp;quot;A chronological timeline of learning discoveries from software development, featuring insights on Python, Django, SQL, and more.&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;* {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-sizing: border-box;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;body {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-family: -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, system-ui, sans-serif;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;min-height: 100vh;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #333;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 1200px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.header p {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;opacity: 0.9;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;max-width: 600px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin: 0 auto;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filters {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: rgba(255,255,255,0.95);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;backdrop-filter: blur(10px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filter-group {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-direction: column;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.filter-label {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filters-container {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-wrap: wrap;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: center;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.4rem 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 20px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 2px solid transparent;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8f9fa;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;cursor: pointer;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: all 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;user-select: none;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transform: translateY(-2px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 4px 8px rgba(0,0,0,0.1);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.category-filter.active {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-color: currentColor;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;bottom: 0;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 2px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(to bottom, #4CAF50, #2196F3, #FF9800, #E91E63);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: relative;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-left: 4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 8px 25px rgba(0,0,0,0.1);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: all 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;opacity: 1;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item.hidden {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: none;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transform: translateY(-5px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 15px 35px rgba(0,0,0,0.15);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;content: &amp;#39;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;position: absolute;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;left: -3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;top: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;width: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;height: 16px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: var(--category-color, #4CAF50);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 3px solid white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 50%;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: flex-start;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-wrap: wrap;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.4rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 600;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #2c3e50;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-decoration: none;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-grow: 1;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;transition: color 0.3s ease;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-title:hover {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #3498db;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: flex;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;align-items: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;flex-shrink: 0;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-date {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: linear-gradient(135deg, #667eea, #764ba2);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.5rem 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 20px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-category {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: var(--category-color, #f8f9fa);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 0.4rem 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 15px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.8rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: 500;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.timeline-description {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #5a6c7d;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;line-height: 1.6;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.loading {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 3rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: white;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 1.2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.error {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: #f8d7da;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #721c24;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 8px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid #f5c6cb;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;background: rgba(255,255,255,0.95);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border-radius: 12px;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-bottom: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;backdrop-filter: blur(10px);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;border: 1px solid rgba(255,255,255,0.2);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stats-grid {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: grid;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;text-align: center;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-item {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-number {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-weight: bold;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #667eea;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;display: block;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;.stat-label {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;color: #666;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;font-size: 0.9rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;margin-top: 0.5rem;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;@media (max-width: 768px) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.container {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.header h1 {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;font-size: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;margin-left: 2rem;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;padding: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-item::before {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;left: -2rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-header {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;flex-direction: column;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;align-items: stretch;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.timeline-meta {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;justify-content: space-between;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.filter-group {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;gap: 1rem;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;.category-filters-container {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;justify-content: center;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;h1&amp;gt;Today I Learned&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;p&amp;gt;A chronological timeline of discoveries, learnings, and insights from my development journey&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stats-grid&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;totalEntries&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Total Entries&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;categoriesCount&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Categories&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;latestEntry&amp;quot;&amp;gt;-&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Latest Entry&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-item&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;stat-number&amp;quot; id=&amp;quot;filteredCount&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Showing&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;filters&amp;quot; id=&amp;quot;filters&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;filter-group&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;filter-label&amp;quot;&amp;gt;Filter by category:&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;category-filters-container&amp;quot; id=&amp;quot;categoryFilters&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;loading&amp;quot; id=&amp;quot;loading&amp;quot;&amp;gt;Loading timeline...&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;error&amp;quot; id=&amp;quot;error&amp;quot; style=&amp;quot;display: none;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline&amp;quot; id=&amp;quot;timeline&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let allEntries = [];&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let categoryColors = {};&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;let activeCategory = null;&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;async function loadTimeline() {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;try {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;// Load timeline data and category colors&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const [entriesResponse, colorsResponse] = await Promise.all([&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;fetch(&amp;#39;timeline-data.json&amp;#39;),&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;fetch(&amp;#39;category-colors.json&amp;#39;)&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;]);&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;if (!entriesResponse.ok || !colorsResponse.ok) {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;throw new Error(&amp;#39;Failed to load timeline data&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries = await entriesResponse.json();&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;categoryColors = await colorsResponse.json();&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayTimeline(allEntries);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;displayStats(allEntries);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;createCategoryFilters();&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;} catch (error) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;console.error(&amp;#39;Error loading timeline:&amp;#39;, error);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;loading&amp;#39;).style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;error&amp;#39;).textContent = &amp;#39;Failed to load timeline. Please try again later.&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function createCategoryFilters() {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const categories = [...new Set(allEntries.map(entry =&amp;gt; entry.category))];&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const filtersContainer = document.getElementById(&amp;#39;categoryFilters&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Add &amp;quot;All&amp;quot; filter&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const allFilter = document.createElement(&amp;#39;span&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.className = &amp;#39;category-filter active&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.textContent = &amp;#39;All&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;allFilter.onclick = () =&amp;gt; filterByCategory(null);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;filtersContainer.appendChild(allFilter);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Add category filters&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;categories.sort().forEach(category =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const filter = document.createElement(&amp;#39;span&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.className = &amp;#39;category-filter&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.textContent = category;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.style.setProperty(&amp;#39;--category-color&amp;#39;, categoryColors[category] || &amp;#39;#666&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.onclick = () =&amp;gt; filterByCategory(category);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filtersContainer.appendChild(filter);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filters&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function filterByCategory(category) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;activeCategory = category;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Update filter button states&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.querySelectorAll(&amp;#39;.category-filter&amp;#39;).forEach(filter =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;filter.classList.remove(&amp;#39;active&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;if ((category === null &amp;amp;&amp;amp; filter.textContent === &amp;#39;All&amp;#39;) ||&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;filter.textContent === category) {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;filter.classList.add(&amp;#39;active&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;if (category !== null) {&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;filter.style.background = categoryColors[category];&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;// Filter timeline items&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const filteredEntries = category ?&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries.filter(entry =&amp;gt; entry.category === category) :&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;allEntries;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;displayTimeline(filteredEntries);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;updateFilteredCount(filteredEntries.length);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function updateFilteredCount(count) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filteredCount&amp;#39;).textContent = count;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayStats(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const categories = [...new Set(entries.map(entry =&amp;gt; entry.category))];&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const latest = entries.length &amp;gt; 0 ? new Date(entries[0].date).toLocaleDateString() : &amp;#39;-&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;totalEntries&amp;#39;).textContent = entries.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;categoriesCount&amp;#39;).textContent = categories.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;latestEntry&amp;#39;).textContent = latest;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;filteredCount&amp;#39;).textContent = entries.length;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;document.getElementById(&amp;#39;stats&amp;#39;).style.display = &amp;#39;block&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;function displayTimeline(entries) {&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineContainer = document.getElementById(&amp;#39;timeline&amp;#39;);&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const loading = document.getElementById(&amp;#39;loading&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;loading.style.display = &amp;#39;none&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;if (entries.length === 0) {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = &amp;#39;&amp;lt;p style=&amp;quot;text-align: center; color: white;&amp;quot;&amp;gt;No entries found.&amp;lt;/p&amp;gt;&amp;#39;;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;const timelineHTML = entries.map(entry =&amp;gt; {&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const date = new Date(entry.date).toLocaleDateString(&amp;#39;en-US&amp;#39;, {&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;year: &amp;#39;numeric&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;month: &amp;#39;short&amp;#39;,&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;day: &amp;#39;numeric&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;const categoryColor = categoryColors[entry.category] || &amp;#39;#666&amp;#39;;&lt;/span&gt;

&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;return `&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-item&amp;quot; style=&amp;quot;--category-color: ${categoryColor}&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-header&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;a href=&amp;quot;${entry.url}&amp;quot; class=&amp;quot;timeline-title&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;${entry.title}&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-meta&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-category&amp;quot; style=&amp;quot;--category-color: ${categoryColor}; background: ${categoryColor};&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                            &lt;/span&gt;&lt;span class="no"&gt;${entry.category}&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;span class=&amp;quot;timeline-date&amp;quot;&amp;gt;${date}&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;div class=&amp;quot;timeline-description&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="no"&gt;${entry.description}&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="no"&gt;`;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;}).join(&amp;#39;&amp;#39;);&lt;/span&gt;

&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="no"&gt;timelineContainer.innerHTML = timelineHTML;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;// Load timeline on page load&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="no"&gt;loadTimeline();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/configure-pages@v4&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Upload artifact&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/upload-pages-artifact@v3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;docs&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github-pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;build-timeline&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Deploy to GitHub Pages&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;deployment&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/deploy-pages@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All told from start to finish, this took about 20 minutes. There are still some potential updates, but for 20 minutes of work I was able to take a 'wild' idea that I would have never been able to do before and had something that I'm actually excited about! And it has the added bonus of encouraging me to write more TILs because I now have &lt;a href="https://ryancheley.github.io/til/"&gt;this nice looking timeline&lt;/a&gt; of my TILs.&lt;/p&gt;</content><category term="technology"></category><category term="til"></category><category term="claude"></category></entry><entry><title>Fun with MCPs</title><link href="https://ryancheley.com/2025/06/02/fun-with-mcps/" rel="alternate"></link><published>2025-06-02T00:00:00-07:00</published><updated>2025-06-02T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-06-02:/2025/06/02/fun-with-mcps/</id><summary type="html">&lt;p&gt;Special Thanks to &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt; who provided an example that really got me started on better understanding of how this all works.&lt;/p&gt;
&lt;p&gt;In trying to wrap my head around MCPs over the long Memorial weekend I had a breakthrough. I'm not really sure why this was so hard for me …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Special Thanks to &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt; who provided an example that really got me started on better understanding of how this all works.&lt;/p&gt;
&lt;p&gt;In trying to wrap my head around MCPs over the long Memorial weekend I had a breakthrough. I'm not really sure why this was so hard for me to &lt;a href="https://en.wikipedia.org/wiki/Grok"&gt;grok&lt;/a&gt;, but now something seems to have clicked.&lt;/p&gt;
&lt;p&gt;I am working with &lt;a href="https://ai.pydantic.dev/"&gt;Pydantic AI&lt;/a&gt; and so I'll be using that as an example, but since MCPs are a standard protocol, these concepts apply broadly across different implementations.&lt;/p&gt;
&lt;h2&gt;What is Model Context Protocol (MCP)?&lt;/h2&gt;
&lt;p&gt;Per the &lt;a href="https://www.anthropic.com/news/model-context-protocol"&gt;Anthropic announcement&lt;/a&gt; (from November 2024!!!!)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Model Context Protocol is an open standard that enables developers to build secure, two-way connections between their data sources and AI-powered tools. The architecture is straightforward: developers can either expose their data through MCP servers or build AI applications (MCP clients) that connect to these servers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What this means is that there is a standard way to extend models like Claude, or OpenAI to include other information. That information can be files on the file system, data in a database, etc.&lt;/p&gt;
&lt;h2&gt;(Potential) Real World Example&lt;/h2&gt;
&lt;p&gt;I work for a Healthcare organization in Southern California. One of the biggest challenges with onboarding new hires (and honestly can be a challenge for people that have been with the organization for a long time) is who to reach out to for support on which specific application.&lt;/p&gt;
&lt;p&gt;Typically a user will send an email to one of the support teams, and the email request can get bounced around for a while until it finally lands on the 'right' support desk. There's the potential to have the applications themselves include who to contact, but some applications are vendor supplied and there isn't always a way to do that.&lt;/p&gt;
&lt;p&gt;Even if there were, in my experience those are often not noticed by users OR the users will think that the support email is for non-technical issues, like "Please update the phone number for this patient" and not issues like, "The web page isn't returning any results for me, but it is for my coworker."&lt;/p&gt;
&lt;h2&gt;Enter an MCP with a Local LLM&lt;/h2&gt;
&lt;p&gt;Let's say you have a service that allows you to search through a file system in a predefined set of directories. This service is run with the following command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;npx&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;--no-cache&lt;span class="w"&gt; &lt;/span&gt;@modelcontextprotocol/server-filesystem&lt;span class="w"&gt; &lt;/span&gt;/path/to/your/files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In Pydantic AI the use of the MCPServerStdio is using this same syntax only it breaks it into two parts&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;command&lt;/li&gt;
&lt;li&gt;args&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The command is any application in your $PATH like &lt;code&gt;uvx&lt;/code&gt; or &lt;code&gt;docker&lt;/code&gt; or &lt;code&gt;npx&lt;/code&gt;, or you can explicitly define where the executable is by calling out its path, like &lt;code&gt;/Users/ryancheley/.local/share/mise/installs/bun/latest/bin/bunx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The args are the commands you'd pass to your application.&lt;/p&gt;
&lt;p&gt;Taking the command from above and breaking it down we can set up our MCP using the following&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;MCPServerStdio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;npx&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;-y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;--no-cache&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;@modelcontextprotocol/server-filesystem&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;/path/to/your/files&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Application of MCP with the Example&lt;/h2&gt;
&lt;p&gt;Since I work in Healthcare, and I want to be mindful of the protection of patient data, even if that data won't be exposed to this LLM, I'll use ollama to construct my example.&lt;/p&gt;
&lt;p&gt;I created a &lt;code&gt;support.csv&lt;/code&gt; file that contains the following information&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Common Name of the Application&lt;/li&gt;
&lt;li&gt;URL of the Application&lt;/li&gt;
&lt;li&gt;Support Email&lt;/li&gt;
&lt;li&gt;Support Extension&lt;/li&gt;
&lt;li&gt;Department&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I used the following prompt&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Review the file &lt;code&gt;support.csv&lt;/code&gt; and help me determine who I contact about questions related to CarePath Analytics.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here are the contents of the &lt;code&gt;support.csv&lt;/code&gt; file&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Support Email&lt;/th&gt;
&lt;th&gt;Support Extension&lt;/th&gt;
&lt;th&gt;Department&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MedFlow Solutions&lt;/td&gt;
&lt;td&gt;https://medflow.com&lt;/td&gt;
&lt;td&gt;support@medflow.com&lt;/td&gt;
&lt;td&gt;1234&lt;/td&gt;
&lt;td&gt;Clinical Systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HealthTech Portal&lt;/td&gt;
&lt;td&gt;https://healthtech-portal.org&lt;/td&gt;
&lt;td&gt;help@medflow.com&lt;/td&gt;
&lt;td&gt;3456&lt;/td&gt;
&lt;td&gt;Patient Services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CarePath Analytics&lt;/td&gt;
&lt;td&gt;https://carepath.io&lt;/td&gt;
&lt;td&gt;support@medflow.com&lt;/td&gt;
&lt;td&gt;4567&lt;/td&gt;
&lt;td&gt;Data Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VitalSign Monitor&lt;/td&gt;
&lt;td&gt;https://vitalsign.net&lt;/td&gt;
&lt;td&gt;support@medflow.com&lt;/td&gt;
&lt;td&gt;1234&lt;/td&gt;
&lt;td&gt;Clinical Systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patient Connect Hub&lt;/td&gt;
&lt;td&gt;https://patientconnect.com&lt;/td&gt;
&lt;td&gt;contact@medflow.com&lt;/td&gt;
&lt;td&gt;3456&lt;/td&gt;
&lt;td&gt;Patient Services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EHR Bridge&lt;/td&gt;
&lt;td&gt;https://ehrbridge.org&lt;/td&gt;
&lt;td&gt;support@medflow.com&lt;/td&gt;
&lt;td&gt;2341&lt;/td&gt;
&lt;td&gt;Integration Services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clinical Workflow Pro&lt;/td&gt;
&lt;td&gt;https://clinicalwf.com&lt;/td&gt;
&lt;td&gt;support@medflow.com&lt;/td&gt;
&lt;td&gt;1234&lt;/td&gt;
&lt;td&gt;Clinical Systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HealthData Sync&lt;/td&gt;
&lt;td&gt;https://healthdata-sync.net&lt;/td&gt;
&lt;td&gt;sync@medflow.com&lt;/td&gt;
&lt;td&gt;6789&lt;/td&gt;
&lt;td&gt;Integration Services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TeleHealth Connect&lt;/td&gt;
&lt;td&gt;https://telehealth-connect.com&lt;/td&gt;
&lt;td&gt;help@medflow.com&lt;/td&gt;
&lt;td&gt;3456&lt;/td&gt;
&lt;td&gt;Patient Services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MedRecord Central&lt;/td&gt;
&lt;td&gt;https://medrecord.central&lt;/td&gt;
&lt;td&gt;records@medflow.com&lt;/td&gt;
&lt;td&gt;5678&lt;/td&gt;
&lt;td&gt;Medical Records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The script is below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# /// script&lt;/span&gt;
&lt;span class="c1"&gt;# requires-python = &amp;quot;&amp;gt;=3.12&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# dependencies = [&lt;/span&gt;
&lt;span class="c1"&gt;#     &amp;quot;pydantic-ai&amp;quot;,&lt;/span&gt;
&lt;span class="c1"&gt;# ]&lt;/span&gt;
&lt;span class="c1"&gt;# ///&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic_ai.mcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MCPServerStdio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic_ai.models.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic_ai.providers.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIProvider&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Configure the Ollama model using OpenAI-compatible API&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OpenAIModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;qwen3:8b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# or whatever model you have installed locally&lt;/span&gt;
        &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;http://localhost:11434/v1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Set up the MCP server to access our support files&lt;/span&gt;
    &lt;span class="n"&gt;support_files_server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MCPServerStdio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;npx&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;-y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;@modelcontextprotocol/server-filesystem&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;/path/to/your/files&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# Directory containing support.csv&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create the agent with the model and MCP server&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mcp_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;support_files_server&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Run the agent with the MCP server&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_mcp_servers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Get response from Ollama about support contact&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;Review the file `support.csv` and help me determine who I contact about questions related to CarePath Analytics?&amp;quot;&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As a user, if I ask, who do I contact about questions related to CarePath Analytics the LLM will search through the &lt;code&gt;support.csv&lt;/code&gt; file and supply the email contact.&lt;/p&gt;
&lt;p&gt;This example shows a command line script, and a Web Interface would probably be better for most users. That would be the next thing I'd try to do here.&lt;/p&gt;
&lt;p&gt;Once that was done you could extend it to also include an MCP to write an email on the user's behalf. It could even ask probing questions to help make sure that the email had more context for the support team.&lt;/p&gt;
&lt;p&gt;Some support systems have their own ticketing / issue tracking systems and it would be really valuable if this ticket could be written directly to that system. With the MCP this is possible.&lt;/p&gt;
&lt;p&gt;We'd need to update the &lt;code&gt;support.csv&lt;/code&gt; file with some information about direct writes via an API, and we'd need to secure the crap out of this, but it is possible.&lt;/p&gt;
&lt;p&gt;Now, the user can be more confident that their issue will go to the team that it needs to and that their question / issue can be resolved much more quickly.&lt;/p&gt;</content><category term="technology"></category><category term="mcp"></category><category term="ollama"></category></entry><entry><title>Firebirds 2024-25 Season</title><link href="https://ryancheley.com/2025/05/21/firebirds-2024-25-season/" rel="alternate"></link><published>2025-05-21T00:00:00-07:00</published><updated>2025-05-21T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-05-21:/2025/05/21/firebirds-2024-25-season/</id><summary type="html">&lt;p&gt;The 2024-25 season for the &lt;a href="https://cvfirebirds.com/"&gt;Coachella Valley Firebirds&lt;/a&gt; ended on &lt;a href="https://theahl.com/stats/game-summary/1027712"&gt;May 9th with a 2-0&lt;/a&gt; loss to the &lt;a href="https://abbotsford.canucks.com/"&gt;Abbotsford Canucks&lt;/a&gt;. Overall, that series saw the Firebirds score&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one goal in &lt;a href="https://theahl.com/stats/game-summary/1027709"&gt;Game 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;one goal in &lt;a href="https://theahl.com/stats/game-summary/1027710"&gt;Game 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;five goals in &lt;a href="https://theahl.com/stats/game-summary/1027711"&gt;Game 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;no goals in &lt;a href="https://theahl.com/stats/game-summary/1027712"&gt;Game 4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn't surprising …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The 2024-25 season for the &lt;a href="https://cvfirebirds.com/"&gt;Coachella Valley Firebirds&lt;/a&gt; ended on &lt;a href="https://theahl.com/stats/game-summary/1027712"&gt;May 9th with a 2-0&lt;/a&gt; loss to the &lt;a href="https://abbotsford.canucks.com/"&gt;Abbotsford Canucks&lt;/a&gt;. Overall, that series saw the Firebirds score&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one goal in &lt;a href="https://theahl.com/stats/game-summary/1027709"&gt;Game 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;one goal in &lt;a href="https://theahl.com/stats/game-summary/1027710"&gt;Game 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;five goals in &lt;a href="https://theahl.com/stats/game-summary/1027711"&gt;Game 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;no goals in &lt;a href="https://theahl.com/stats/game-summary/1027712"&gt;Game 4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn't surprising given exactly how young the Firebirds were this season, but it was disappointing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Derek_Laxdal"&gt;Coach Laxdal&lt;/a&gt; talked a lot about how young the team was and how on any given night we would have anywhere from seven to nine rookies that were in the starting lineup. And in a team of 24, that's a pretty big portion of guys out there who are very young.&lt;/p&gt;
&lt;p&gt;That being said the disappointment is palpable the this is the earliest that the Firebirds have ever exited the postseason. Granted this is only their third year but we are typically used to seeing hockey for another seven weeks. When put into that perspective, it is really disappointing.&lt;/p&gt;
&lt;p&gt;Still, I think there were some really bright spots from this year, including &lt;a href="https://theahl.com/stats/player/10083/88/lleyton-roed"&gt;Leyton Roed&lt;/a&gt;, &lt;a href="https://theahl.com/stats/player/10127/88/jani-nyman"&gt;Jani Nyman&lt;/a&gt;, &lt;a href="https://theahl.com/stats/player/10186/88/nikke-kokko"&gt;Nikke Kokko&lt;/a&gt;, &lt;a href="https://theahl.com/stats/player/9766/88/ryan-winterton"&gt;Ryan Winterton&lt;/a&gt;, and &lt;a href="https://theahl.com/stats/player/9764/88/ty-nelson"&gt;Ty Nelson&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At the start of the season, I did indicate to a friend of mine (who also has season tickets) that I had pretty low expectations for the Firebirds and may have even indicated I wasn't sure that they would make the playoffs. The Pacific Division has 10 teams and 7 of them make the playoffs. I may have been a bit too pesimisitic in that analysis.&lt;/p&gt;
&lt;p&gt;During the first round the Firebirds swept the Wrangerls 2-0. This is great, but they did manage to blow a 3-0 lead in &lt;a href="https://theahl.com/stats/game-summary/1027696"&gt;game 1&lt;/a&gt;. The were able to win that game, but it took two plus Overtime periods (it ended a few minutes into the third OT).&lt;/p&gt;
&lt;p&gt;Game two of that series did see the Firebirds win 2-0 with Nikke Kokko getting his first professional AHL shutout, which was great . But it's also a bummer that it took until the 74th game of the season for him to get his first shutout of the season. &lt;sup id="sf-firebirds-2024-25-season-1-back"&gt;&lt;a href="#sf-firebirds-2024-25-season-1" class="simple-footnote" title="During the regular season, there was exactly one shutout by Victor Ostman"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;In six games, the Firebirds were 3-3. They scored four goals, two goals, one goal, five goals, one goal, and no goals. They were 0-17 on the power play, and they gave up two, count them two, 3 goal leads.&lt;/p&gt;
&lt;p&gt;Needless to say, this was just a hard set of games to watch. The season was hard to watch as a fan. The Firebirds would find ways to lose games. In previous seasons these were the games that they would find some way to win!&lt;/p&gt;
&lt;p&gt;There was an article in the Desert Sun that spoke about how proud Coach Laxdal was of the players and how much effort that they gave. And I agree, they did give a lot of effort and he spoke about how young they are.&lt;/p&gt;
&lt;p&gt;And again, they are young, and missed their captain &lt;a href="https://theahl.com/stats/player/5611"&gt;Max McCormick&lt;/a&gt; for basically two thirds of the season. But they did have some veteran players out there &lt;a href="https://theahl.com/stats/player/6306/88/mitchell-stephens"&gt;Mitchell Stephens&lt;/a&gt;, &lt;a href="https://theahl.com/stats/player/8513/88/brandon-biro"&gt;Brandon Biro&lt;/a&gt;, &lt;a href="https://theahl.com/stats/player/7382/88/cale-fleury"&gt;Cale Fleury&lt;/a&gt; and &lt;a href="https://theahl.com/stats/player/5471/88/gustav-olofsson"&gt;Gustav Olofsson&lt;/a&gt;. Unfortunately it was just too much to try and overcome.&lt;/p&gt;
&lt;p&gt;One of the things that Coach Laxdal also commented on was exactly how much younger next year's team might be. And so while I am again very excited about watching hockey in six months, which is just so long away. I am lowering my expectations for the 25-26 season even lower than they were this year. I'm really hoping we make the playoffs, but won't be surprised if we don't.&lt;/p&gt;
&lt;p&gt;And that's going to be okay ... because even bad hockey is still hockey. And I love hockey, and even when they lose, I love watching the Firebirds.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-firebirds-2024-25-season-1"&gt;During the regular season, there was exactly one shutout by Victor Ostman &lt;a href="#sf-firebirds-2024-25-season-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="hockey"></category><category term="firebirds"></category></entry><entry><title>Uptime Kuma and Claude</title><link href="https://ryancheley.com/2025/05/08/uptime-kuma-and-claude/" rel="alternate"></link><published>2025-05-08T00:00:00-07:00</published><updated>2025-05-08T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-05-08:/2025/05/08/uptime-kuma-and-claude/</id><summary type="html">&lt;p&gt;While talking to a colleague of mine he had mentioned something called &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt;, which is an open source alternative to Uptime Robot. This was interesting for me because I have Uptime Robot checking a couple of my websites and I thought, "Hey, maybe I can use Uptime Kuma as …&lt;/p&gt;</summary><content type="html">&lt;p&gt;While talking to a colleague of mine he had mentioned something called &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt;, which is an open source alternative to Uptime Robot. This was interesting for me because I have Uptime Robot checking a couple of my websites and I thought, "Hey, maybe I can use Uptime Kuma as a replacement for Uptime Robot."&lt;/p&gt;
&lt;p&gt;I dove into the &lt;a href="https://github.com/louislam/uptime-kuma/blob/master/README.md"&gt;docs&lt;/a&gt; a bit and discovered that there was a Docker image for it, which was nice.&lt;/p&gt;
&lt;p&gt;I then used Claude to take the &lt;code&gt;docker run&lt;/code&gt; command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nv"&gt;restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;always&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;:&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;uptime&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;kuma&lt;/span&gt;:&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;uptime&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;kuma&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;louislam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;uptime&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;kuma&lt;/span&gt;:&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and turn it into a compose file, which gave me this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;version: '3'

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: always
    ports:
      &lt;span class="k"&gt;-&lt;/span&gt; "3001:3001"
    volumes:
      &lt;span class="k"&gt;-&lt;/span&gt; uptime-kuma:/app/data

volumes:
  uptime-kuma:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, I took the output of that compose file and asked Claude to also generate a caddy configuration for me and it gave me this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yourdomain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Enable compression&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Add security headers&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Enable HSTS&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Transport&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max-age=31536000; includeSubDomains; preload"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Prevent content-type sniffing&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nosniff"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Referrer policy&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Referrer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strict-origin-when-cross-origin"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Reverse proxy to Uptime Kuma&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;reverse_proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I then SSH'd into one of my servers using &lt;a href="https://secureshellfish.app/"&gt;Shellfish&lt;/a&gt; &lt;sup id="sf-uptime-kuma-and-claude-1-back"&gt;&lt;a href="#sf-uptime-kuma-and-claude-1" class="simple-footnote" title="this is an amazing app on the iPad, highly recommend"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I updated the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and my &lt;code&gt;Caddyfile&lt;/code&gt; to include what Claude had output.&lt;/p&gt;
&lt;p&gt;I restarted my docker containers and didn't get my new container running.&lt;/p&gt;
&lt;p&gt;So I took the whole Docker Compose file from my server and I put that into Claude and said,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hey, is there anything wrong with my Docker Compose file?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It indicated that there were some issues and provided updates for. I made those changes and did the same thing with the &lt;code&gt;Caddyfile&lt;/code&gt;. Again, Claude offered up some changes. I applied the recommended changes for the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and the &lt;code&gt;Caddyfile&lt;/code&gt; stopped and started my docker containers.&lt;/p&gt;
&lt;p&gt;I suddenly had an instance of Uptime Kuma. All in all, it took about a half hour from start to finish while I was watching a &lt;a href="https://theahl.com/stats/game-center/1027706"&gt;hockey game&lt;/a&gt; ... from my iPad.&lt;/p&gt;
&lt;p&gt;I didn't really have to do anything other than a couple of tweaks here and there on the Docker Compose file and a couple of tweaks here and there on the Caddyfile. and I suddenly have this tool that allows me to monitor the uptime of various websites that I'm interested in.&lt;/p&gt;
&lt;p&gt;As I wrapped up it hit me ... holy crap, this is an amazing time to live&lt;sup id="sf-uptime-kuma-and-claude-2-back"&gt;&lt;a href="#sf-uptime-kuma-and-claude-2" class="simple-footnote" title="Yes, there's also some truly horrific shit going on too"&gt;2&lt;/a&gt;&lt;/sup&gt;. You have an idea, Claude (or whatever AI tool you want to use) outputs a thing, and then you're up and running. This really reduces that barrier to entry to just try new things.&lt;/p&gt;
&lt;p&gt;Is the Docker Compose file the most performant? I don't know. Is the Caddyfile the most secured lockdown thing? I don't know.&lt;/p&gt;
&lt;p&gt;But for these small projects that are just me, I don't know how much it really matters.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-uptime-kuma-and-claude-1"&gt;this is an amazing app on the iPad, highly recommend &lt;a href="#sf-uptime-kuma-and-claude-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-uptime-kuma-and-claude-2"&gt;Yes, there's also some truly horrific shit going on too &lt;a href="#sf-uptime-kuma-and-claude-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="oss"></category><category term="monitoring"></category></entry><entry><title>The Invisible Decision-Makers: Why Systems Ignore Their Users</title><link href="https://ryancheley.com/2025/03/31/the-invisible-decision-makers-why-systems-ignore-their-users/" rel="alternate"></link><published>2025-03-31T00:00:00-07:00</published><updated>2025-03-31T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-03-31:/2025/03/31/the-invisible-decision-makers-why-systems-ignore-their-users/</id><summary type="html">&lt;h2&gt;The Origin of Systems&lt;/h2&gt;
&lt;p&gt;When thinking about systems it's easy to think that they have always been there, or been that way. This isn't true of course. The systems that are in place were put there, by people. People that made decisions. Decisions are what  I want to focus on …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;The Origin of Systems&lt;/h2&gt;
&lt;p&gt;When thinking about systems it's easy to think that they have always been there, or been that way. This isn't true of course. The systems that are in place were put there, by people. People that made decisions. Decisions are what  I want to focus on here.&lt;/p&gt;
&lt;p&gt;In general when making a decision about the implementation of a system you would want to engage with the stakeholders of that system. This of course implies that you can identify at least some of those stakeholders.&lt;/p&gt;
&lt;p&gt;But sometimes there aren't any key stakeholders other than regulations, or best practice, or some other nebulous thing that needs to be met. These are the decisions I really want to focus on.&lt;/p&gt;
&lt;h2&gt;The Illusion of Success&lt;/h2&gt;
&lt;p&gt;Take a security system for instance. The basic tenets of the security system are that it keeps 'something' safe. If the thing to be kept safe is still safe after the implementation of the security system then the people that implemented the system can claim success. They can look at the evidence that since the security system was put into place the thing has been kept safe.&lt;/p&gt;
&lt;p&gt;Of course, it's entirely possible that the thing was never in danger, and that the previous system was doing just fine. In fact, it could be that the security system is actually making it harder to keep the thing safe. It's just harder to see because all you're looking at are potentially meaningless metrics like. Questions like is the thing safe after implementation of the security system don't take into account if the thing was 'unsafe' before? This can lead you to think that the new security system must be responsible for the safety of the thing.&lt;/p&gt;
&lt;p&gt;Something else that can be happening is that the security system has caused the people responsible for keeping the thing safe to work more hours, hire more people,who are oftentimes  keeping the security system running.&lt;/p&gt;
&lt;h2&gt;Questioning Purpose&lt;/h2&gt;
&lt;p&gt;The more we look into a system like this, the more we might ask, "Why is it there?"&lt;/p&gt;
&lt;p&gt;There can be a couple of reasons, but I'll focus on one in particular. The person ultimately responsible for keeping the thing safe can show with some kind of metrics that the thing is safe with the new security system, whereas they couldn't under the previous system. There weren't any reports or metrics that showed what was going on, which is why the system was implemented in the first place.&lt;/p&gt;
&lt;p&gt;OK ... so that's how some systems can be put in place.&lt;/p&gt;
&lt;h2&gt;User-Hostile Systems&lt;/h2&gt;
&lt;p&gt;What about systems that are hard to use, or maybe actively hostile to their users? How do those get put into place? I would argue that the reason we see  many user hostile systems in place is because they are decided upon not by their users, but by their ability to meet regulations, AND their ability to maintain by a support system. The consideration of the user is secondary, or maybe not even thought about.&lt;/p&gt;
&lt;p&gt;Think about any Enterprise software you've ever encountered. Would you say that it was a joy to use? Would you say that onboarding was simple, and that new employees loved to use it? My guess would be no.&lt;/p&gt;
&lt;h2&gt;Why Bad Systems Persist&lt;/h2&gt;
&lt;p&gt;So if the users don't like it, why is it in place? Two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It meets some kind of regulation (this could be a government regulation, but it could also be a regulation of a guild, or union, or something else)&lt;/li&gt;
&lt;li&gt;It's easy to maintain by the support system&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For any software that meets these criteria you are likely to have users that don't like the software, because they are always an afterthought. The primary responsibility of the software developers of these types of systems is always the regulators, and the support infrastructure.&lt;/p&gt;
&lt;p&gt;The first because they have to keep producing software that is compliant in order to be sold with a specific rating or seal of approval.&lt;/p&gt;
&lt;p&gt;The second because if the support team can't easily support it, they're going to find an alternative solution that they can support.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It's a simple decision of maximizing for the people that enforce the rules (regulators) and that make the decisions (support). The users of the software don't matter. At all.&lt;/p&gt;
&lt;p&gt;This is why you will see software for widget processing that could benefit from bulk operations,  keyboard shortcuts, or being browser agnostic and they just aren't. The only considerations are: Does it meet the regulations? Is it easy to support? If the answers are yes then the users tend to lose out. They don't matter. If the answer is no, then find a competitor that does and move over to them, even if the current system is loved by your users.&lt;/p&gt;</content><category term="musings"></category><category term="systems"></category></entry><entry><title>Process, People, and Priorities</title><link href="https://ryancheley.com/2025/03/09/Process-People-and-Priorities/" rel="alternate"></link><published>2025-03-09T00:00:00-08:00</published><updated>2025-03-09T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-03-09:/2025/03/09/Process-People-and-Priorities/</id><summary type="html">&lt;p&gt;In every organization, three critical elements determine success: People, Processes, and Priorities. While all are essential, their ranking matters profoundly. Based on my experience across several organizations, I've found that Processes must come first, followed by People, with Priorities anchored firmly at the foundation.&lt;/p&gt;
&lt;p&gt;This deliberate ordering—Processes at the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In every organization, three critical elements determine success: People, Processes, and Priorities. While all are essential, their ranking matters profoundly. Based on my experience across several organizations, I've found that Processes must come first, followed by People, with Priorities anchored firmly at the foundation.&lt;/p&gt;
&lt;p&gt;This deliberate ordering—Processes at the top, People in the middle, and Priorities as bedrock—creates the most stable and effective organizational structure. When Processes guide how People work and how Priorities are determined, organizations can avoid the chaos of constant priority shifts, reduce dependency on specific individuals, and create consistent frameworks for decision-making.&lt;/p&gt;
&lt;h2&gt;Defining Terms&lt;/h2&gt;
&lt;p&gt;Let's define what each of these mean from an organizational perspective:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Processes - How to solve the problems&lt;/li&gt;
&lt;li&gt;People - Who will solve the problems&lt;/li&gt;
&lt;li&gt;Priorities - The order in which to solve the problems&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Process&lt;/h2&gt;
&lt;p&gt;In my experience ranking Priorities first leads to lots of changes to Priorities. This week it's shipping a new feature to make all of the buttons cornflower blue ... next week it's adding AI to the application. The week after that it's to mine bitcoin. Priorities shift, and that's OK, but priority driven organizations seem to not have a true defining north star to help guide them, which in my experience that leads to chaos.&lt;/p&gt;
&lt;p&gt;Ranking People first sounds like a good idea. I mean, who doesn't want to put People first? I have found however that when People are prioritized first bad things can happen. Cliques can form. Only Sally can do thing X and they're out for the next three weeks and no, there isn't any documentation on how to do that. Management can be lax because that's just Bob being Bob and can lead to toxic work environments.&lt;/p&gt;
&lt;p&gt;I think that putting Process first helps to mitigate, though not outright eliminate, these concerns.&lt;/p&gt;
&lt;p&gt;Processes help to determine how we do thing &lt;strong&gt;X&lt;/strong&gt;. If Sally is out, that's OK because we have a &lt;em&gt;Process&lt;/em&gt; and documentation to help us through it. Will we get it done as quickly as Sally would have gotten it done? No, but we will get it done before they come back.&lt;/p&gt;
&lt;p&gt;Processes also help implement things like Codes of Conduct. Again, that won't prevent cliques from forming, and no it won't keep Bob from being a jerk, but it creates a framework to help deal with Bob being a jerk and potentially removing them from the situation entirely.&lt;/p&gt;
&lt;p&gt;Processes can also help with prioritization. Having a Process that helps to guide HOW you prioritize can be very helpful. This doesn't prevent you from switching up your Priorities, but it does help to keep you focused on something long enough to complete it. And when you need to change a priority it's a lot easier (and healthier) to be able to point to the Process that drove the deicsion to change versus a statement like, "I don't know, the CEO saw something on Bloomberg and now we're doing this."&lt;/p&gt;
&lt;p&gt;Setting up Processes is hard. And in a small environments it can seem like it's not worth it. For example, asking "Why do we have a 17 page document that talks about how Priorities are chosen if it's just a handful of People?" Yes, that IS hard. And it might not seem like it's worth it. But you don't need a big long document to determine a Process on how to change Priorities. It can be as simple as&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We are small and acknowledge that change is required. We will only change when a consensus of 60% of the team agree with the change OR if the CEO and CFO agree on the change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;More complicated Processes can come later. But at least now when a change is needed you know HOW you're going to talk about that change!&lt;/p&gt;
&lt;h2&gt;People&lt;/h2&gt;
&lt;p&gt;What comes second? I find that People should be next. It's the People that are going to help make everything happen. It's the People that are going to help get you over the finish line of the projects that are driven by your Processes. It's People that will work the Processes.&lt;/p&gt;
&lt;p&gt;Once you have good Processes and good People, then you can really start to set Priorities that EVERYONE will understand.&lt;/p&gt;
&lt;h3&gt;An Example&lt;/h3&gt;
&lt;p&gt;My least favorite answer to the question, "Why do we do it this way?" is "I don't know."&lt;/p&gt;
&lt;p&gt;In my opinion this points to a broken culture. It could be that when you started you did ask questions, but you were shot down so many time for asking that you just stopped asking. It could be that you're not very curious and someone just told you and didn't provide a reason and you just accepted it as gospel that this is the way that it needs to be done.&lt;/p&gt;
&lt;p&gt;The reason why this is a toxic trait is that you can have a situation like this occur&lt;/p&gt;
&lt;p&gt;While working on a report a requester indicated that the margins weren't quite right and it was VERY important that they be 'just so'. I met with the requester and asked them about the Process and it went something like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mermaid.live/edit#pako:eNpVkMtqwzAQRX9FzNoOjvxqvCgUSqCLQKGr1OpiGo1iU1kKiozjhvx75aRpk4Vg7pmDdNERNlYSVKC0HTYNOi_MTtWvrjWeLa3rPoRRrfm0h3pKrDXsZUpnrEkFtqqXYWLX9coa3zBltSR3Y63vrTWh-5e8w31TP3lGRjKr2DhtfePswHDA8ZL_7Kkhi2OJrR7j-JFd-gUyEH1d0W-3QLup0D1eB4zG9Kgv_Py-MBBBR67DVoYPOQrDmADfUEcCqjBKUthrL0CYU1Cx9_ZtNBuovOspgn4n0dNzi1uH3RXu0LxbexuhOsIBKs7TGedZXmQ8S_IkS8oIxoDLWZHMebFYhFOWGT9F8H2-IJk95ClP87RIF0XCi2IegbP9toFKod7T6Qc7uJk4"&gt;&lt;img alt="" src="https://mermaid.ink/img/pako:eNpVkMtqwzAQRX9FzNoOjvxqvCgUSqCLQKGr1OpiGo1iU1kKiozjhvx75aRpk4Vg7pmDdNERNlYSVKC0HTYNOi_MTtWvrjWeLa3rPoRRrfm0h3pKrDXsZUpnrEkFtqqXYWLX9coa3zBltSR3Y63vrTWh-5e8w31TP3lGRjKr2DhtfePswHDA8ZL_7Kkhi2OJrR7j-JFd-gUyEH1d0W-3QLup0D1eB4zG9Kgv_Py-MBBBR67DVoYPOQrDmADfUEcCqjBKUthrL0CYU1Cx9_ZtNBuovOspgn4n0dNzi1uH3RXu0LxbexuhOsIBKs7TGedZXmQ8S_IkS8oIxoDLWZHMebFYhFOWGT9F8H2-IJk95ClP87RIF0XCi2IegbP9toFKod7T6Qc7uJk4?type=png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I drew out the flow and asked the requester why, they said, "I don't know, that's just how Tim trained me"&lt;/p&gt;
&lt;p&gt;I was fortunate that Tim was still at the company, so I called him and asked about the Process.&lt;/p&gt;
&lt;p&gt;He laughed and said something to the effect of, "They're still doing that? I only had that in place because of an issue with a fax machine 8 years ago but IT fixed it. Why are they still doing it that way?"&lt;/p&gt;
&lt;p&gt;"Because that's how they were trained"&lt;/p&gt;
&lt;p&gt;🤦🏻‍♂️&lt;/p&gt;
&lt;p&gt;Always understand why you're doing a thing. Always. This points to the need for Process, and why I place it first. Process matters and it helps to inform the People what they need to do.&lt;/p&gt;
&lt;h2&gt;Priorities&lt;/h2&gt;
&lt;p&gt;Why are Priorities last? How can something as important as Priorities be last?&lt;/p&gt;
&lt;p&gt;I would argue that Priorities should be the bedrock of you organization and they should be HARD to change. Constantly shifting Priorities leads to dissatisfaction, and burnout. It can also lead People to wonder if what they do actually matters. If it's always changing, why should I care about what I'm working on right now if it's just going to be different later today, tomorrow, or next week.&lt;/p&gt;
&lt;p&gt;The interplay between Processes, People, and Priorities forms the backbone of any effective organization. By putting Processes first, we create the infrastructure that enables People to thrive and Priorities to remain stable. Good Processes provide clarity, continuity, and a framework for decision-making that transcends individual preferences or momentary urgencies.&lt;/p&gt;
&lt;p&gt;When organizations understand that Priorities should be difficult to change—and that a clear Process should govern how and when they change—they protect their teams from the whiplash of constant redirection. This stability doesn't mean rigidity; rather, it ensures that when change does occur, it happens deliberately, transparently, and with organizational buy-in.&lt;/p&gt;
&lt;p&gt;Whether you're leading a startup of five People or managing departments within a large corporation, begin by examining your Processes. Are they documented? Do People understand not just what to do, but why? Is there a clear Process for establishing and modifying Priorities? If you can answer "yes" to these questions, you've laid the groundwork for an organization where People can contribute meaningfully to Priorities that truly matter.&lt;/p&gt;
&lt;p&gt;Remember: Process first, People second, and Priorities as the bedrock. Get this order right, and you'll build an organization that can handle change without losing its way.&lt;/p&gt;</content><category term="management"></category><category term="process"></category><category term="people"></category><category term="priorities"></category></entry><entry><title>Technical Solutions to People Problems</title><link href="https://ryancheley.com/2025/02/06/technical-solutions-to-people-problems/" rel="alternate"></link><published>2025-02-06T00:00:00-08:00</published><updated>2025-02-06T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-02-06:/2025/02/06/technical-solutions-to-people-problems/</id><summary type="html">&lt;blockquote&gt;
&lt;p&gt;"If you think technology will solve your problems, you don't understand technology and you don't understand your problems"&lt;/p&gt;
&lt;p&gt;~ attrib. Laurie Anderson&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;From a &lt;a href="https://mas.to/@natureworks/113917094844091858"&gt;Toot&lt;/a&gt; by &lt;a href="https://mas.to/@natureworks"&gt;Jake Rayson&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In a previous post, I wrote about how to &lt;a href="https://www.ryancheley.com/2024/08/22/how-to-ask-why-without-sounding-like-a-jerk/"&gt;ask why without sounding like a jerk&lt;/a&gt;. This is a slightly related concept (at …&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;"If you think technology will solve your problems, you don't understand technology and you don't understand your problems"&lt;/p&gt;
&lt;p&gt;~ attrib. Laurie Anderson&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;From a &lt;a href="https://mas.to/@natureworks/113917094844091858"&gt;Toot&lt;/a&gt; by &lt;a href="https://mas.to/@natureworks"&gt;Jake Rayson&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In a previous post, I wrote about how to &lt;a href="https://www.ryancheley.com/2024/08/22/how-to-ask-why-without-sounding-like-a-jerk/"&gt;ask why without sounding like a jerk&lt;/a&gt;. This is a slightly related concept (at least in my head).&lt;/p&gt;
&lt;p&gt;Sometimes, as technical people, we are asked to solve problems. The more we dig into them, the more we discover that the problem that needs to be solved isn't a technical one but a people one. In many cases, it's just getting two groups to actually talk to one another.&lt;/p&gt;
&lt;p&gt;This can be hard and awkward, so people may want to avoid it. Creating a report telling someone they're doing something wrong is way easier. No hurt feelings! However, I've found that the approach tends to create more problems than it solves.&lt;/p&gt;
&lt;h2&gt;The situation&lt;/h2&gt;
&lt;p&gt;The situation is a real one, and I'm obfuscating details to help 'protect the innocent'.&lt;/p&gt;
&lt;p&gt;At the start of each year, large amounts of new data are needed to be added to a system. The additions are, by their nature, very manual&lt;sup id="sf-technical-solutions-to-people-problems-1-back"&gt;&lt;a href="#sf-technical-solutions-to-people-problems-1" class="simple-footnote" title="yes I would like to automate this, but one step at a time!"&gt;1&lt;/a&gt;&lt;/sup&gt; and so the team responsible for them spends much of their time trying to get the data added.&lt;/p&gt;
&lt;p&gt;Another team is highly dependent on this new data being added in order to process their widgets&lt;sup id="sf-technical-solutions-to-people-problems-2-back"&gt;&lt;a href="#sf-technical-solutions-to-people-problems-2" class="simple-footnote" title="Not actually widgets"&gt;2&lt;/a&gt;&lt;/sup&gt;. The widgets get loaded into the system and checked to see if the data from team A is complete. If it isn't, then the widget gets flagged. This flag directs the members of Team B to reach out to Team A to get clarification on the state of the data needed to process the widget.&lt;/p&gt;
&lt;p&gt;Only, that's not how Team A understands it. While they are furiously trying to update data, there is some basic data that covers roughly 80% of the widget processing data needs that are already available. So, the vast majority of the time, there is no need for Team B to reach out to Team A because the information they need is available in the system to process the widget.&lt;/p&gt;
&lt;p&gt;This understanding was either lost or never communicated effectively so Team B would just email Team A with questions about the widget data and then get their answer and move on. This is despite the fact that the information is available in the system for the members of Team B to review!&lt;/p&gt;
&lt;p&gt;The leader of Team A asked me if I could 'update a report' to 'remove some of these widgets so Team A could better focus on the work of adding the data'.&lt;/p&gt;
&lt;p&gt;I thought that seemed reasonable, so I asked Team B a few questions and then made a bit more discoveries and found out the actual problem, which was that the information needed by Team B was in the system. Team A just needed Team B to do a better job of looking for it and asking questions about the things that were needed instead of everything.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;I proposed that the leaders from Team A and Team B get together to talk about the issue.&lt;/p&gt;
&lt;p&gt;At the meeting, the leader of Team B was horrified to hear what was happening. They had no idea that many emails were going to Team A about questions that the members of Team B should be able to answer on their own.&lt;/p&gt;
&lt;p&gt;This is all well and good, but why did it take a tech person to spot this and get the team leadership together to figure it out?&lt;/p&gt;
&lt;p&gt;I wish I &lt;strong&gt;knew&lt;/strong&gt; the answer. I think part of the insight I had was the current pipeline of work, how long it was going to create a report, and the need to have the problem solved sooner rather than later didn't line up. At all.&lt;/p&gt;
&lt;p&gt;I &lt;strong&gt;needed&lt;/strong&gt; to look for a potential non-technical solution. The other thing that I think happened here is that I wasn't weighed down by any history of interactions between the Teams. I was just trying to gather information. In gathering information I was able to see what the real problem was and that the only solution that made sense was for the two teams to just talk to each other.&lt;/p&gt;
&lt;h2&gt;The Outcome&lt;/h2&gt;
&lt;p&gt;During the meeting, Team B committed to retraining staff and helping to make sure that they only reached out when there was an actual question about the data for the widget production. Team A was thrilled with this solution, and now they can focus on getting the data into the system more efficiently and with fewer interruptions. A win-win, all because a tech guy got some non-tech people to talk to one another.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-technical-solutions-to-people-problems-1"&gt;yes I would like to automate this, but one step at a time! &lt;a href="#sf-technical-solutions-to-people-problems-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-technical-solutions-to-people-problems-2"&gt;Not actually widgets &lt;a href="#sf-technical-solutions-to-people-problems-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="solutions"></category></entry><entry><title>How to Watch a Hockey Game - Reading the Standings</title><link href="https://ryancheley.com/2025/02/03/how-to-watch-a-hockey-game-reading-the-standings/" rel="alternate"></link><published>2025-02-03T00:00:00-08:00</published><updated>2025-02-03T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-02-03:/2025/02/03/how-to-watch-a-hockey-game-reading-the-standings/</id><summary type="html">&lt;p&gt;This is the fourth part of my How to Watch a Hockey Game Series. You can catch up on previous articles &lt;a href="https://www.ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Game Outcomes&lt;/h2&gt;
&lt;p&gt;In many North American sports when reading the standings there are typically just Wins (W), and Losses (L).&lt;sup id="sf-how-to-watch-a-hockey-game-reading-the-standings-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-reading-the-standings-1" class="simple-footnote" title="Football also has Ties (T) but they are exceedingly rare and are only ever displayed when the first Tie of the season occurs"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Hockey is a bit different. When you …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is the fourth part of my How to Watch a Hockey Game Series. You can catch up on previous articles &lt;a href="https://www.ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Game Outcomes&lt;/h2&gt;
&lt;p&gt;In many North American sports when reading the standings there are typically just Wins (W), and Losses (L).&lt;sup id="sf-how-to-watch-a-hockey-game-reading-the-standings-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-reading-the-standings-1" class="simple-footnote" title="Football also has Ties (T) but they are exceedingly rare and are only ever displayed when the first Tie of the season occurs"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Hockey is a bit different. When you look at the standings for Hockey you'll see 4 headers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W: Wins&lt;/li&gt;
&lt;li&gt;L: Losses&lt;/li&gt;
&lt;li&gt;OTL: Overtime Losses&lt;/li&gt;
&lt;li&gt;SOL: Shootout Losses&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As discussed &lt;a href="https://www.ryancheley.com/2025/01/29/how-to-watch-a-hockey-game-game-play/"&gt;earlier in this series&lt;/a&gt;, if a game is tied at the end of regulation, a five-minute overtime period is played. If either team scores during this Overtime period then the winning team gets a Win, while the losing team gets an Overtime Loss (OTL).&lt;/p&gt;
&lt;p&gt;If they're still tied then a Shootout is played. Once a winner is declared in the Shootout they get the Win, while the losing team gets a Shootout Loss.&lt;/p&gt;
&lt;p&gt;Because of this, values are assigned to each type of outcome:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Win&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loss&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This might best be shown with a concrete example.&lt;/p&gt;
&lt;h2&gt;A Concrete Example&lt;/h2&gt;
&lt;p&gt;Let's say that the Coachella Valley Firebirds have played 39 games so far. They have won 21 games and lost 13 games. They've also played in 5 games that went into overtime and lost. Their overtime losses are one (1) in the Overtime period and 4 in Shootouts. Their record would look like this:&lt;/p&gt;
&lt;p&gt;Coachella Valley Firebirds: 21-13-1-4&lt;/p&gt;
&lt;p&gt;Points Calculation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wins: 21 × 2 = 42 points&lt;/li&gt;
&lt;li&gt;OTL: 1 × 1 = 1 point&lt;/li&gt;
&lt;li&gt;SOL: 4 × 1 = 4 points&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total: 42 + 1 + 4 = 47 points&lt;/p&gt;
&lt;p&gt;The Firebirds play in the Pacific Division of the Western Conference, and the standings might look like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;GP&lt;/th&gt;
&lt;th&gt;W&lt;/th&gt;
&lt;th&gt;L&lt;/th&gt;
&lt;th&gt;OTL&lt;/th&gt;
&lt;th&gt;SOL&lt;/th&gt;
&lt;th&gt;PTS&lt;/th&gt;
&lt;th&gt;PCT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calgary&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;0.671&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coachella Valley&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;0.603&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colorado&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;0.639&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ontario&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;0.622&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;0.597&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abbotsford&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;0.568&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tucson&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0.541&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bakersfield&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;0.529&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Diego&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;0.378&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Henderson&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Legend:
- GP: Games Played
- W: Wins
- L: Losses
- OTL: Overtime Losses
- SOL: Shootout Losses
- PTS: Points
- PCT: Points Percentage&lt;/p&gt;
&lt;h2&gt;Winning Percent&lt;/h2&gt;
&lt;p&gt;There are 2 things to look at in the standings: (1) Total Points, and (2) Winning Percent.&lt;/p&gt;
&lt;p&gt;The Total Points we've already spoken about so let's review winning percent.&lt;/p&gt;
&lt;p&gt;The winning percent is calculated as the Total Points the team has divided by the total possible points that they could have gotten. The total possible points are calculated as the Games Played x 2 (that is, what are the total number of points that they would have if they won every game they played).&lt;/p&gt;
&lt;p&gt;That is&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    Winning Percent = Total Points ÷ (Games Played × 2)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For example in the table above, we see that the PCT column for the Firebirds is 0.603. This is calculated by the Points (47) divided by GP x 2 (39 x 2 = 78), that is 47 / 78 = 0.603.&lt;/p&gt;
&lt;p&gt;The winning percent allows ranking intra-season when teams haven't played the same number of games. After all games have been played, the rankings are determined by the total number of points a team has.&lt;sup id="sf-how-to-watch-a-hockey-game-reading-the-standings-2-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-reading-the-standings-2" class="simple-footnote" title="Depending on the league there are tiebreakers, but that's outside the scope of this article"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;You should now be able to parse the standings in a Hockey League and be able to tell how well (or poorly) your team is doing.&lt;/p&gt;
&lt;p&gt;This is the end of my series (for now). If there are any other burning questions you have about hockey, reach out to me on &lt;a href="https://mastodon.social/@ryancheley"&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-to-watch-a-hockey-game-reading-the-standings-1"&gt;Football also has Ties (T) but they are exceedingly rare and are only ever displayed when the first Tie of the season occurs &lt;a href="#sf-how-to-watch-a-hockey-game-reading-the-standings-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-reading-the-standings-2"&gt;Depending on the league there are tiebreakers, but that's outside the scope of this article &lt;a href="#sf-how-to-watch-a-hockey-game-reading-the-standings-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="hockey"></category></entry><entry><title>How to Watch a Hockey Game - What to Watch</title><link href="https://ryancheley.com/2025/01/31/how-to-watch-a-hockey-game-what-to-watch/" rel="alternate"></link><published>2025-01-31T00:00:00-08:00</published><updated>2025-01-31T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-31:/2025/01/31/how-to-watch-a-hockey-game-what-to-watch/</id><summary type="html">&lt;p&gt;In &lt;a href="https://www.ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/"&gt;a previous post of this series&lt;/a&gt; I laid out some basic rules of hockey. In this post I'll hopefully provide some tips on what to watch during your first few hockey games.&lt;/p&gt;
&lt;h2&gt;What should I 'watch' though?&lt;/h2&gt;
&lt;p&gt;This is a tough question and depends on if you're watching on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In &lt;a href="https://www.ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/"&gt;a previous post of this series&lt;/a&gt; I laid out some basic rules of hockey. In this post I'll hopefully provide some tips on what to watch during your first few hockey games.&lt;/p&gt;
&lt;h2&gt;What should I 'watch' though?&lt;/h2&gt;
&lt;p&gt;This is a tough question and depends on if you're watching on TV or in person.&lt;/p&gt;
&lt;h3&gt;On TV&lt;/h3&gt;
&lt;p&gt;If you're watching on TV you're limited by whatever the camera and director are showing you. Hopefully they're pretty good at what they do and they'll help to show you what is interesting. You'll also have the benefit of replays. &lt;sup id="sf-how-to-watch-a-hockey-game-what-to-watch-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-what-to-watch-1" class="simple-footnote" title="and refreshments that are much less expensive!"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Watching the action on TV will be your best bet. The commentators will do a reasonable job of explaining the play. For some of the best NHL broadcasts you'll want to watch a Canadian feed. This might not be an option depending on where you live, but in general, watching a Canadian feed of a Canadian team will be really helpful.&lt;/p&gt;
&lt;p&gt;If, for whatever reason, you're watching an AHL game&lt;sup id="sf-how-to-watch-a-hockey-game-what-to-watch-2-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-what-to-watch-2" class="simple-footnote" title="home of my beloved Coachella Valley Firebirds"&gt;2&lt;/a&gt;&lt;/sup&gt; the best broadcasts to watch, in my opinion, are the Lehigh Valley Phantoms called by &lt;a href="https://www.phantomshockey.com/staff/bob-rotruck/"&gt;Bob Rotruck&lt;/a&gt; and Cleveland Monsters called by &lt;a href="https://www.tonybrownpxp.com/"&gt;Tony Brown&lt;/a&gt;. Each of these is a single broadcaster doing both the color commentary and the play-by-play ... and they honestly get &lt;strong&gt;so&lt;/strong&gt; excited it's hard to NOT get excited with them.&lt;/p&gt;
&lt;h3&gt;In Person&lt;/h3&gt;
&lt;p&gt;For your first in person game, just try and follow the puck as best you can. If for whatever reason you can't do that, pick a spot on the ice to concentrate on, preferably near one of the goalies. Which one? The goalie of the team you're not rooting for is a good choice! Then you can just kind of watch the action there.&lt;/p&gt;
&lt;p&gt;Keeping in mind &lt;a href="https://www.ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/"&gt;the rules&lt;/a&gt; start by focusing on just one rule - either icing or offside - for an entire period. Once you feel comfortable recognizing that rule during gameplay, switch your attention to watching for the other rule in the next period. For example, if you spent the first period watching for icing, spend the next period looking for offside plays.&lt;/p&gt;
&lt;p&gt;Hopefully after a full game you're able to see them when icing or offside happen. If not, it just means you'll need to come back and try again 😁.&lt;/p&gt;
&lt;h2&gt;What not to worry about&lt;/h2&gt;
&lt;p&gt;Hockey is a fast paced game. No, like really fast. Don't worry too much about anything other than watching for the puck, if you can, and trying to pick up icing and offside. You'll see other stoppages in play when a penalty is called. The refs will make &lt;a href="https://www.chicagowolves.com/gameday/hockey-101/penalties-and-signals/"&gt;hand gestures&lt;/a&gt; to indicate the call on the ice and someone will be sent to the box.&lt;/p&gt;
&lt;p&gt;Don't worry about whether or not a fight will break out. They don't always, and if they do, each player will be assessed a major penalty and will spend 5+ minutes in the penalty box.&lt;/p&gt;
&lt;p&gt;Don't worry too much about learning the positions. The goalie is an obvious one (that's the person with all of the pads, the bigger stick, and the giant, well painted mask in front of the net), but trying to distinguish between a defender and a center ... like just don't worry about it!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hockey is an amazing sport to watch, whether in person or on TV. It can take a little bit of time to get used to the fast pace, but hopefully this series has given you some tips to enjoy it and understand what's going on.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-to-watch-a-hockey-game-what-to-watch-1"&gt;and refreshments that are much less expensive! &lt;a href="#sf-how-to-watch-a-hockey-game-what-to-watch-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-what-to-watch-2"&gt;home of my beloved Coachella Valley Firebirds &lt;a href="#sf-how-to-watch-a-hockey-game-what-to-watch-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="hockey"></category></entry><entry><title>How to Watch a Hockey Game - Game Play</title><link href="https://ryancheley.com/2025/01/29/how-to-watch-a-hockey-game-game-play/" rel="alternate"></link><published>2025-01-29T00:00:00-08:00</published><updated>2025-01-29T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-29:/2025/01/29/how-to-watch-a-hockey-game-game-play/</id><summary type="html">&lt;h2&gt;Game Structure&lt;/h2&gt;
&lt;p&gt;Hockey has some stuff in common with live theater. No ... really! 😁&lt;/p&gt;
&lt;p&gt;They both have dressing rooms and they both have intermission ... but that is probably where the similarities end.&lt;/p&gt;
&lt;p&gt;Each hockey game is split into three 20 minute periods. There is an intermission between each period that lasts …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Game Structure&lt;/h2&gt;
&lt;p&gt;Hockey has some stuff in common with live theater. No ... really! 😁&lt;/p&gt;
&lt;p&gt;They both have dressing rooms and they both have intermission ... but that is probably where the similarities end.&lt;/p&gt;
&lt;p&gt;Each hockey game is split into three 20 minute periods. There is an intermission between each period that lasts 18 minutes. During the intermission the players go back to the dressing room to regroup and chat about the previous period a strategize for the upcoming period.&lt;/p&gt;
&lt;p&gt;Out in the arena there are chances for you to get overpriced refreshments, stand in long lines to use the facilities, or just stay in your seat and watch the silly intermission games.&lt;/p&gt;
&lt;p&gt;Some examples I've seen of silly intermission games are Fuego Pong (like quarters, but with soccer balls and large 5 gallon buckets), ice bowling where a player is put into a giant slingshot on the ice and hudled towards inflatable bowling pins, and the dress up game.&lt;/p&gt;
&lt;p&gt;It's also during this time that the ice is resurfaced by a &lt;a href="https://en.m.wikipedia.org/wiki/Ice_resurfacer"&gt;Zamboni&lt;/a&gt; to make it nice and clean for the next period.&lt;/p&gt;
&lt;p&gt;If at the end of the third period the game is tied then you're in luck because you get free hockey, also known as Overtime. One thing to keep in mind is that the overtime rules during a regular season game are different than a postseason game.&lt;/p&gt;
&lt;h3&gt;Regular Season Overtime Rules&lt;/h3&gt;
&lt;p&gt;At the end of the third period there is a 1 minute 'intermission' and then a 5 minute overtime period starts. The overtime period will feature 3 skaters from each team as well as their goalie.&lt;/p&gt;
&lt;p&gt;If a penalty occurs in Overtime (or is carried over from the third period) the period starts with four players on the power play team and 3 on the short handed team.&lt;sup id="sf-how-to-watch-a-hockey-game-game-play-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-game-play-1" class="simple-footnote" title="essentially it would be a short Overtime period and probably pretty boring"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Each team tries to score a goal first. If they do, then they win in overtime. If, at the end of 5 minutes of play, the score is still tied then a shootout happens.&lt;/p&gt;
&lt;p&gt;In the shootout each team has 3 chances to score a &lt;a href="https://en.wikipedia.org/wiki/Penalty_shot_(ice_hockey)"&gt;penalty shot&lt;/a&gt;. Essentially a skater from each team has the opportunity to try and score a goal with only the goalie trying to prevent it. If at the end of the three rounds we're still tied, we keep sending out skaters to try and get that penalty shot until one team is victorious. The record for most rounds of a shoot out is &lt;a href="https://youtu.be/oH79V8zcMKk?si=pZYQ0ANCpsPrt-5z"&gt;20 rounds&lt;/a&gt; in the NHL, and 16 rounds in the AHL.&lt;/p&gt;
&lt;h3&gt;Postseason Overtime Rules&lt;/h3&gt;
&lt;p&gt;Postseason overtime rules are a bit different. Basically you just keep adding 20 minute periods until someone scores. Once a team scores they have won that game. The longest overtime in NHL Postseason history went into the 6th overtime and was &lt;a href="https://records.nhl.com/records/playoff-team-records/overtime/longest-overtime-playoff"&gt;played in 1936&lt;/a&gt; between the Detroit Red Wings and the Montreal Maroons. The longest AHL overtime was between the &lt;a href="https://www.phantomshockey.com/timeline-relive-longest-game-ahl-history/#:~:text=The%20game%2C%20which%20took%20place,series%20lead%20over%20the%20Checkers"&gt;Charlotte Checkers and the Lehigh Valley Phantoms&lt;/a&gt; which went into a 5th overtime period. This game started at 7:03 pm local and didn't finish until almost 3:00 am local the next day!&lt;/p&gt;
&lt;p&gt;In general most hockey games don't get past the first OT period. From The 2006 playoffs through to the 2024 playoffs there have only been 52 games that have gone into a second overtime period (out of &lt;a href="https://ahl-data.ryancheley.com/games?sql=select%0D%0A++g.game_status%0D%0A++%2C+min%28g.game_date%29%0D%0A++%2C+count%28%2A%29%0D%0Afrom%0D%0A++games+g%0D%0Ainner+join+dim_date+as+d+on+g.game_date+%3D+d.date%0D%0Awhere+d.season_phase+%3D+%27post%27%0D%0Agroup+by+g.game_status%0D%0Aorder+by+g.game_status&amp;amp;_hide_sql=1"&gt;1312&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;OK, you've got a few basics 'under your belt'. In the next part I'll try and answer the question, 'What should I watch?'.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-to-watch-a-hockey-game-game-play-1"&gt;essentially it would be a short Overtime period and probably pretty boring &lt;a href="#sf-how-to-watch-a-hockey-game-game-play-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="hockey"></category></entry><entry><title>How to Watch a Hockey Game - Three Rules</title><link href="https://ryancheley.com/2025/01/27/how-to-watch-a-hockey-game-three-rules/" rel="alternate"></link><published>2025-01-27T00:00:00-08:00</published><updated>2025-01-27T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-27:/2025/01/27/how-to-watch-a-hockey-game-three-rules/</id><summary type="html">&lt;p&gt;I've written a few times before about hockey. I love watching my local sports puck team&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-1" class="simple-footnote" title="The Coachella Valley Firebirds"&gt;1&lt;/a&gt;&lt;/sup&gt; and really wish more people watched it. So, I'm going to write a beginners guide to watching hockey so that you too, dear reader, can become an avid fan.&lt;/p&gt;
&lt;p&gt;Hockey is a pretty …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've written a few times before about hockey. I love watching my local sports puck team&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-1-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-1" class="simple-footnote" title="The Coachella Valley Firebirds"&gt;1&lt;/a&gt;&lt;/sup&gt; and really wish more people watched it. So, I'm going to write a beginners guide to watching hockey so that you too, dear reader, can become an avid fan.&lt;/p&gt;
&lt;p&gt;Hockey is a pretty fast paced game at the professional level. In the 90s Fox Sports had broadcast rights to hockey in the US and to help its viewers they had a glowing halo on the puck called &lt;a href="https://en.wikipedia.org/wiki/FoxTrax"&gt;FoxTrax&lt;/a&gt; which allowed fans to more easily find it. This practice was discontinued at some point, and I honestly think it was one of the better innovations that Fox Sports did and really wish that it would make a come back.&lt;/p&gt;
&lt;h2&gt;The Rules&lt;/h2&gt;
&lt;p&gt;As a beginner hockey observer there's only three rules that you really need to know to be able to follow the game.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Offside&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-2-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-2" class="simple-footnote" title="in hockey it is not pluralized like in American Football ... even though in American Football it's not pluralized either!"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Icing&lt;/li&gt;
&lt;li&gt;Power Play / Penalty Kill&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Set up&lt;/h3&gt;
&lt;p&gt;The ice rink can be broken into 3 sections from the perspective of 1 team. Let's assume we have two teams, A and B. Let's root for team A.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ice Hockey Rink" src="https://www.conceptdraw.com/How-To-Guide/picture/Sport-Hockey-Simple-hockey-field-Template.png" title="Ice Hockey Rink Diagram"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Defending zone - This is where team A's Goal is located. It starts right behind team A's goal and goes to the right toward the blue line&lt;/li&gt;
&lt;li&gt;Neutral Zone - This is the center of the ice between the two blue lines; it also contains a red line that is called 'Center Ice'&lt;/li&gt;
&lt;li&gt;The Attacking Zone - This is where team A are trying to score. It starts at the OTHER blue line and goes back behind Team B's goal&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Offside&lt;/h3&gt;
&lt;p&gt;Offside is defined as ... actually that's not important. What is important to understand is that a player on the offense cannot enter their Attacking zone before the puck does. If they do, then that player is called Offside. When an Offside happens a face off takes place outside of the Attacking zone (i.e. in the Neutral Zone) where each team will try and gain control of the puck.&lt;/p&gt;
&lt;h3&gt;Icing&lt;/h3&gt;
&lt;p&gt;Icing, or icing the puck, is when a player in their half of the ice and shots the puck down the ice towards their Attacking zone and it is NOT touched by anyone before it passes the face off circles in the Attacking zone. When an icing occurs the puck is returned to the defending zone for a face off&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-3-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-3" class="simple-footnote" title="This does NOT apply when your team is on a Penalty Kill"&gt;3&lt;/a&gt;&lt;/sup&gt;. When an icing occurs the team that the icing is called on have to keep all of their players on the ice, that is, they can not send in any substitutions.&lt;/p&gt;
&lt;h3&gt;Power Play / Penalty Kill&lt;/h3&gt;
&lt;p&gt;The two rules above, when broken, result in a stoppage of play and a new face off for each team to try to gain control of the puck. Other rules, when broken, will result in a penalty&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-4-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-4" class="simple-footnote" title="I'll talk more about various penalties in future a post"&gt;4&lt;/a&gt;&lt;/sup&gt; which sees one, or more, players sent to the Penalty Box&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-5-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-5" class="simple-footnote" title="it's a small room where players are sent to think about what they did"&gt;5&lt;/a&gt;&lt;/sup&gt;. Penalties can either be minor, which result in a two minute penalty, or major, which typically result in a 5 minute penalty&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-6-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-6" class="simple-footnote" title="There are a few caveats here about game misconduct, but they're not important for an introductory primer"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When a team is on the Power Play they will have 1 or more extra skaters than the other team. The other team's 'missing' players will be in the Penalty Box. The Power Play team, with the advantage, will remain on that advantage until either they score OR the penalty expires. If a team scores while on the Power Play, they are said to have scored a Power Play Goal.&lt;/p&gt;
&lt;p&gt;The team that has penalized players is said to be on the Penalty Kill. They are trying to 'kill' the advantage that the Power Play brings to the other team. If the team on the Penalty Kill scores a goal, it is called a Short-handed goal ... because they were short a person, i.e. short handed, when the goal was scored. In the &lt;a href="https://www.nhl.com/"&gt;National Hockey League&lt;/a&gt; (NHL), &lt;a href="https://theahl.com/"&gt;American Hockey League&lt;/a&gt; (AHL), and most other leagues when a short handed goal is scored the Penalty keeps going until time is over OR a goal is scored by the team on the Power Play. The &lt;a href="https://www.thepwhl.com/en/"&gt;Professional Women's Hockey Leagure&lt;/a&gt; (PWHL) has a rule (which I think is genius) which states that IF a team scores a short handed goal, the Power Play is over.&lt;sup id="sf-how-to-watch-a-hockey-game-three-rules-7-back"&gt;&lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-7" class="simple-footnote" title="Now, there are lots of Nuances to the PP/PK write up above, but you don't need to understand them initially to enjoy hockey. "&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;In the next post I'll talk a bit more about game play.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-1"&gt;The &lt;a href="https://cvfirebirds.com"&gt;Coachella Valley Firebirds&lt;/a&gt; &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-2"&gt;in hockey it is not pluralized like in American Football ... even though in American Football it's not pluralized either! &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-3"&gt;This does NOT apply when your team is on a Penalty Kill &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-4"&gt;I'll talk more about various penalties in future a post &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-5"&gt;it's a small room where players are sent to think about what they did &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-6"&gt;There are a few caveats here about game misconduct, but they're not important for an introductory primer &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-6-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-how-to-watch-a-hockey-game-three-rules-7"&gt;Now, there are lots of Nuances to the PP/PK write up above, but you don't need to understand them initially to enjoy hockey.  &lt;a href="#sf-how-to-watch-a-hockey-game-three-rules-7-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="hockey"></category></entry><entry><title>Remember the Colosseum!</title><link href="https://ryancheley.com/2025/01/21/remember-the-colosseum/" rel="alternate"></link><published>2025-01-21T00:00:00-08:00</published><updated>2025-01-21T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-21:/2025/01/21/remember-the-colosseum/</id><summary type="html">&lt;h2&gt;The Roman Colosseum&lt;/h2&gt;
&lt;p&gt;After the fall of the Western Roman Empire in 497 CE the Colosseum fell into disrepair. Rightfully so! Who can worry about keeping up a giant megalith made by people centuries ago while you're just trying to figure out where your next meal may come from, or …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;The Roman Colosseum&lt;/h2&gt;
&lt;p&gt;After the fall of the Western Roman Empire in 497 CE the Colosseum fell into disrepair. Rightfully so! Who can worry about keeping up a giant megalith made by people centuries ago while you're just trying to figure out where your next meal may come from, or the ranging hordes of barbarians showing up and taking the food you did find!&lt;/p&gt;
&lt;p&gt;However, during the medieval period, while Rome's population declined dramatically and many ancient structures fell into disrepair or were repurposed, the Colosseum remained a prominent landmark. There are stories that as the centuries progressed, the inhabitants of Rome forgot who built it.  While some fantastical legends did develop around it, the basic historical facts of its construction by the Flavian emperors and its original purpose remained part of common knowledge among educated Romans. For the non-educated Roman's there were lots of misconceptions about the colosseum.&lt;/p&gt;
&lt;p&gt;The non-education Romans would have created stories&lt;sup id="sf-remember-the-colosseum-1-back"&gt;&lt;a href="#sf-remember-the-colosseum-1" class="simple-footnote" title="such as it being a temple to the sun"&gt;1&lt;/a&gt;&lt;/sup&gt; about the large building. It was haunted. It was used for pagan rituals and no good Christian would go in. Folklore would rise up around it. As many of us have seen or experienced, in the absence of information, people will make it up.&lt;sup id="sf-remember-the-colosseum-2-back"&gt;&lt;a href="#sf-remember-the-colosseum-2" class="simple-footnote" title="Brené Brown"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;The Story of the Legacy System&lt;/h2&gt;
&lt;p&gt;OK, but why is this important from a technology perspective?&lt;/p&gt;
&lt;p&gt;Imagine if you will a large system, built 10 years ago, by a group of developers, that have all left the organization.&lt;/p&gt;
&lt;p&gt;No one left knows how it works, or how to make changes to it. Most people don't even really know WHY it's there in the first place.&lt;/p&gt;
&lt;p&gt;There isn't any documentation that can be referred to. Either because it wasn't ever created OR it was destroyed by Barbarians, I mean well meaning IT processes that 'clean up' unused files.&lt;/p&gt;
&lt;p&gt;So what happens? The people remaining create stories about the system. Stories like the long timer 'Bob' that once caused the entire system to Crash and then an old copy backup copy had to be restored, and months worth of work was lost.&lt;/p&gt;
&lt;p&gt;No one ever saw Bob after that. Now we're all afraid to touch any part of it. We mostly leave it alone, and it leaves us alone.&lt;/p&gt;
&lt;p&gt;There are stories about another gray beard that actually built the system, but everyone assumes these are just fairy tales.&lt;/p&gt;
&lt;p&gt;The stories tell of this Gray Beard busting out the entire system in a weekend, using nothing but a pin to move the electrons into the proper places to get all of the logic to work as expected.&lt;/p&gt;
&lt;p&gt;Of course, no one really believes that story, but it encourages people to never want to have to make and changes to it.&lt;/p&gt;
&lt;p&gt;The problem here is that it's running on a server with an OS that hasn't been supported for 7 years and there is security mandate to upgrade 'everything' to be on current software&lt;/p&gt;
&lt;p&gt;No one wants to be in charge of this project, but someone is going to have to be in charge of it.&lt;/p&gt;
&lt;p&gt;What do you do?&lt;/p&gt;
&lt;p&gt;The story above isn't real, at least not for me. But it could be.&lt;/p&gt;
&lt;p&gt;How many times have you gotten to a system that is old, no one around has any idea how it was built and people mostly just avoid it? Probably more than once.&lt;/p&gt;
&lt;p&gt;But how can we avoid this fate? Do we just keep the old timers on until they (or the system) die?&lt;/p&gt;
&lt;p&gt;There are options, and they are some of the easiest things to do, but many people don't like to do them.&lt;/p&gt;
&lt;p&gt;What is the answer?&lt;/p&gt;
&lt;h2&gt;Documentation&lt;/h2&gt;
&lt;p&gt;Documentation. No really, Documentation. Just write it down. For a new project especially. For an old project? Most definitely.&lt;/p&gt;
&lt;p&gt;For new projects it's best to just get into the habit of writing good docs&lt;sup id="sf-remember-the-colosseum-3-back"&gt;&lt;a href="#sf-remember-the-colosseum-3" class="simple-footnote" title="any docs in this case are good docs!"&gt;3&lt;/a&gt;&lt;/sup&gt; as you go. If that's doc strings in a method, or a full fledged Knowledge Management System using a documentation framework like &lt;a href="https://diataxis.fr/"&gt;diataxis&lt;/a&gt;, then so be it.&lt;/p&gt;
&lt;p&gt;But write it down. Write down the why's whenever you can. Use something like an &lt;a href="https://www.cognitect.com/blog/2011/11/15/documenting-architecture-decisions"&gt;Architectural Decision Dsocument&lt;/a&gt; to understand WHY you made a technical decision you made. Maybe it's not the best decision, but it's the best decision given a set of constraints.&lt;/p&gt;
&lt;p&gt;For existing projects, it can be more challenging. It's possible that NO ONE that created the system is at the organization. It could be that NO ONE that asked for the system to be created is at the organization.&lt;/p&gt;
&lt;p&gt;This leads to a bunch of problems to try to solve, but the journey of 1000 miles starts with a single step.&lt;/p&gt;
&lt;h2&gt;How do you solve it?&lt;/h2&gt;
&lt;p&gt;Use the helpful &lt;a href="https://en.wikipedia.org/wiki/There_are_unknown_unknowns"&gt;Awareness-Understanding Matrix&lt;/a&gt;&lt;sup id="sf-remember-the-colosseum-4-back"&gt;&lt;a href="#sf-remember-the-colosseum-4" class="simple-footnote" title="This is in no way an endoresement of Donald Rumsfeld. He was a horrible person"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Aware&lt;/th&gt;
&lt;th&gt;Unaware&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Understand&lt;/td&gt;
&lt;td&gt;Known Knowns&lt;/td&gt;
&lt;td&gt;Unknowns Known&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't Understand&lt;/td&gt;
&lt;td&gt;Known Unknowns&lt;/td&gt;
&lt;td&gt;Unknown Unknowns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That is,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Known Knowns: Things we are aware of and understand&lt;/li&gt;
&lt;li&gt;Known Unknowns: Things we are aware of but don't understand&lt;/li&gt;
&lt;li&gt;Unknown Knowns: Things we are not aware of but do understand or know implicitly&lt;/li&gt;
&lt;li&gt;Unknown Unknowns: Things we are neither aware of nor understand&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Known Knowns may be very small, but it won't be empty.&lt;/p&gt;
&lt;p&gt;The Unknown Unknowns might (will probably) be the largest.&lt;/p&gt;
&lt;p&gt;The lack of knowledge here represents Risk&lt;sup id="sf-remember-the-colosseum-5-back"&gt;&lt;a href="#sf-remember-the-colosseum-5" class="simple-footnote" title="Jacob Kaplan-Moss has a great series on Risk"&gt;5&lt;/a&gt;&lt;/sup&gt;. Risk to your team, or to your organization. This Risk needs to be handled as much as possible.&lt;/p&gt;
&lt;p&gt;Looking at a system with the Awareness-Understanding Matrix can help to risk it properly. Once you've properly risked the system, then you can start writing documentation.&lt;/p&gt;
&lt;p&gt;The documentation can take the form of Architectural Review of System X (DRAFT)&lt;/p&gt;
&lt;p&gt;The system does these things&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Thing 1&lt;/li&gt;
&lt;li&gt;Thing 2&lt;/li&gt;
&lt;li&gt;Many other things that are still unknown&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Sometimes just the act of writing these things out will help you in finding out what you know and what you don't know.&lt;/p&gt;
&lt;p&gt;If you're using a documentation framework like &lt;a href="https://diataxis.fr/"&gt;diataxis&lt;/a&gt; for this, you will want to keep your documentation parts separated (How To, Tutorials, Reference, Explanation). You may start righting a Reference article on the system and realize that you also need to have some, yet to be discovered, Explanation. The issue is that the Explanation still needs to be researched and written.&lt;/p&gt;
&lt;p&gt;That's OK! One strategy I've encouraged, and use, is if I'm writing a Reference Article and need to link to a yet to be written Explanation article, is that I'll simply create the yet to be written Explanation article and tag it with &lt;code&gt;Explanation&lt;/code&gt; and &lt;code&gt;Stub&lt;/code&gt;. This frees me to come back to it later and fill in the details.&lt;/p&gt;
&lt;p&gt;The other thing that will need to be done is to figure out who uses the system. Sometimes that's super easy, and sometimes, it's not.&lt;/p&gt;
&lt;p&gt;Once you're able to determine who uses the system, you can talk with them about the system and then work to fill in the gaps from above.&lt;/p&gt;
&lt;p&gt;Occasionally, you find out who everyone &lt;em&gt;thinks&lt;/em&gt; is using the system, and discover that actually, it hasn't been used for 5 years because &lt;strong&gt;reasons&lt;/strong&gt;, and they didn't know who to tell.&lt;/p&gt;
&lt;p&gt;Now you can just retire the system using a decommissioning process. You have a technology decommissioning process, right? If you don't, it may be time to look into one!&lt;/p&gt;
&lt;h2&gt;Back to the colosseum&lt;/h2&gt;
&lt;p&gt;The inhabitants of Rome never got to a spot where none of them knew why it was built, or who built it. Or even why. But what did happen is that the people with the knowledge may have been parts of groups that were marginalized and therefore their knowledge was discounted or ignored. Because the knowledge was a verbal knowledge and not written down. It was, to use a loaded term, tribal knowledge. EVERYONE just knows the obvious thing.&lt;/p&gt;
&lt;p&gt;But the thing is ... obvious things are only obvious in the context they were created. It's obvious what Python is. I mean, why would someone use a snake to write code to get a computer to do a thing. EVERYONE knows I'm talking about the programming language Python ... until they don't.&lt;/p&gt;
&lt;p&gt;Just write this shit down. Make sure everyone gets into the habit of documenting. Make the documentation public. And if it's not possible to make all of the documentation public, make as much public as possible.&lt;/p&gt;
&lt;p&gt;For the parts that aren't public, make sure they are accessible by the people that will need access to it.&lt;/p&gt;
&lt;p&gt;Really, documentation is a means to an end. Sometimes you won't need the documentation. You'll know how the thing works, and it has an obvious API or UI and people just "get it". This can lead to people not writing the documentation because we don't need it.&lt;/p&gt;
&lt;p&gt;This is kind of like saying, I've used a seatbelt every day for 30 years and I've never needed it. I don't see why I need to wear it any more.&lt;/p&gt;
&lt;p&gt;This might be fine until you're in an accident.&lt;/p&gt;
&lt;p&gt;Not writing documentation is fine, until it's needed. And that's the worst time to discover that you need it.&lt;/p&gt;
&lt;p&gt;Better to have it and not need it, than to need it and not have it.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-remember-the-colosseum-1"&gt;such as it being a temple to the sun &lt;a href="#sf-remember-the-colosseum-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-remember-the-colosseum-2"&gt;Brené Brown &lt;a href="#sf-remember-the-colosseum-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-remember-the-colosseum-3"&gt;any docs in this case are good docs! &lt;a href="#sf-remember-the-colosseum-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-remember-the-colosseum-4"&gt;This is in no way an endoresement of Donald Rumsfeld. He was a horrible person &lt;a href="#sf-remember-the-colosseum-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-remember-the-colosseum-5"&gt;Jacob Kaplan-Moss has a great series on &lt;a href="https://jacobian.org/series/risk/"&gt;Risk&lt;/a&gt; &lt;a href="#sf-remember-the-colosseum-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="documentation"></category></entry><entry><title>Looking Back at the Half Marathon Streak</title><link href="https://ryancheley.com/2025/01/17/looking-back-at-the-half-marathon-streak/" rel="alternate"></link><published>2025-01-17T00:00:00-08:00</published><updated>2025-01-17T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-17:/2025/01/17/looking-back-at-the-half-marathon-streak/</id><summary type="html">&lt;h1&gt;Looking Back at the Half Marathon Streak&lt;/h1&gt;
&lt;h2&gt;How It Started&lt;/h2&gt;
&lt;p&gt;In February 2012, a half marathon was being held in Palm Springs, and one of my walking friends asked if I wanted to do it... about 5 days before it was set to happen. I said I wasn't interested, and …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;Looking Back at the Half Marathon Streak&lt;/h1&gt;
&lt;h2&gt;How It Started&lt;/h2&gt;
&lt;p&gt;In February 2012, a half marathon was being held in Palm Springs, and one of my walking friends asked if I wanted to do it... about 5 days before it was set to happen. I said I wasn't interested, and she said "Where is your sense of adventure?" ... though it was in slightly more colorful language. Six days later, I was participating in my first half marathon.&lt;/p&gt;
&lt;p&gt;Now, I hadn't really run too much up to that point. A 5K here and there, and for the most part, I wasn't running much during those "runs".&lt;/p&gt;
&lt;p&gt;At the start of the half marathon, I looked at my friend that had persuaded me to participate and said something like, "We're just going to walk, right?" She said we had to run at least some of it.&lt;/p&gt;
&lt;p&gt;Eight miles later, I was still running and enjoying it. She wanted to walk a bit. So we did, and then she encouraged me to start running again, so I did.&lt;/p&gt;
&lt;h2&gt;The First Taste of Success&lt;/h2&gt;
&lt;p&gt;I finished that first half marathon in something like 2h30m, but I was suddenly hooked. I thought, "I bet if I actually &lt;em&gt;trained&lt;/em&gt; for a half marathon, I could do better." So I set out to train for a half marathon. I started a training schedule using &lt;a href="https://www.halhigdon.com/training-programs/half-marathon-training/novice-1-half-marathon/"&gt;this plan&lt;/a&gt; and was able to complete the San Diego Rock 'n Roll half in a little more than 2 hours. I continued to run medium distances (up to 12 miles on weekends) and really enjoyed it.&lt;/p&gt;
&lt;p&gt;I got to a point where I was in the best shape of my life. I had a resting heart rate of something like 50 with blood pressure that was pretty low (almost low enough that my doctor was concerned!) but my labs were good, and I felt GREAT!&lt;/p&gt;
&lt;h2&gt;The Birth of the Streak&lt;/h2&gt;
&lt;p&gt;I was looking at half marathons in 2014 and saw the Palm Springs one again, but also saw one in a local city called &lt;a href="https://www.cityofdhs.org/"&gt;Desert Hot Springs&lt;/a&gt; in December 2013. Then there was the Carlsbad half in January 2014. Another one looked interesting in Zion National Park (sort of) in March of 2014. And then I saw that there was the La Jolla half in April.&lt;/p&gt;
&lt;p&gt;It also turns out that if you run the Carlsbad, La Jolla, and America's Finest City (AFC) half marathons, you get a nice triple crown medal. Well, the AFC was in August. I was looking at the calendar and thought, "Holy shit, I might be able to schedule half marathons each month for an entire year!"&lt;/p&gt;
&lt;h2&gt;The Journey Begins&lt;/h2&gt;
&lt;p&gt;In November of 2013, I signed up for a couple of the half marathons that were further out (essentially the ones for the Triple Crown medal) and the Palm Springs half.&lt;/p&gt;
&lt;p&gt;I wanted to do the Zion run, but it's a 10-12 hour drive to Zion from where I live. I casually mentioned this to an acquaintance who said, "I'd run that with you."&lt;/p&gt;
&lt;p&gt;The next thing I knew, I had a pretty good string of runs set up: (1) Carlsbad, (2) Palm Springs, (3) Zion, (4) La Jolla, (5) America's Finest City ... but I still hadn't told anyone, other than my wife, about my crazy idea to run a half marathon each month.&lt;/p&gt;
&lt;h2&gt;Making the Commitment&lt;/h2&gt;
&lt;p&gt;One day while on a walk with a friend, I mentioned that I was thinking about doing it and he said, "That's an awesome idea... you should totally do it." We spent most of the walk with me talking about the idea and wavering until I finally just said, "I'm going to do it."&lt;/p&gt;
&lt;p&gt;And that was it. I had made a commitment, publicly, about this thing I was going to do.&lt;/p&gt;
&lt;h2&gt;The Year of Running&lt;/h2&gt;
&lt;p&gt;All that was left was to finalize each of the runs, and then train. And by train, I mean run. A lot. So much running.&lt;/p&gt;
&lt;p&gt;I ended up running 13 half marathons in 364 days, bookending the feat with the Half Marathon in Desert Hot Springs in December of 2013 and 2014.&lt;/p&gt;
&lt;p&gt;In all, I ran in the following cities (some still have my results online!):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Desert Hot Springs (twice!) in December (2013, &lt;a href="https://my.racewire.com/result/3182333"&gt;2014&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://my.racewire.com/athlete/5131448"&gt;Carlsbad&lt;/a&gt; in January&lt;/li&gt;
&lt;li&gt;Palm Springs in February&lt;/li&gt;
&lt;li&gt;Zion in March&lt;/li&gt;
&lt;li&gt;&lt;a href="https://my.racewire.com/result/5108370"&gt;La Jolla&lt;/a&gt; in April&lt;/li&gt;
&lt;li&gt;Menifee in May&lt;/li&gt;
&lt;li&gt;Rock n Roll in San Diego in June&lt;/li&gt;
&lt;li&gt;Oceanside in July&lt;/li&gt;
&lt;li&gt;&lt;a href="https://my.racewire.com/result/5064858"&gt;San Diego (AFC)&lt;/a&gt; in August&lt;/li&gt;
&lt;li&gt;Ventura in September&lt;/li&gt;
&lt;li&gt;San Luis Obispo (SLO) in October&lt;/li&gt;
&lt;li&gt;Santa Barbara in November&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During that year I ran roughly 1000 miles training, and 170.3 miles for the actual runs.&lt;/p&gt;
&lt;h2&gt;The Reality of the Challenge&lt;/h2&gt;
&lt;p&gt;I have never been so tired in all of my life as I was that year, but I look back on it and it was really fun.&lt;/p&gt;
&lt;p&gt;At the beginning of the year, I had an idea that I would get better and faster at running. That didn't happen&lt;sup id="sf-looking-back-at-the-half-marathon-streak-1-back"&gt;&lt;a href="#sf-looking-back-at-the-half-marathon-streak-1" class="simple-footnote" title="just take a look at my available results above via the links!"&gt;1&lt;/a&gt;&lt;/sup&gt; for a variety of reasons, but mostly because it wasn't fun. What was fun was going to different places with my family where the races were being held and hanging out with them. I still have fond memories of being in Ventura and going to a thrift store with my wife and daughter and finding a pair of roller skates that fit my daughter perfectly.&lt;/p&gt;
&lt;h2&gt;The Memorable Moments&lt;/h2&gt;
&lt;p&gt;I remember the kitschy B&amp;amp;B in Santa Barbara where my wife and I stayed and walking around Santa Barbara and finding this cool co-op style building with lots of vendors for getting food, drinks, or artisanal handcrafted stuff.&lt;/p&gt;
&lt;p&gt;I also remember running the La Jolla half with a low-grade fever and realizing exactly how bad of an idea that was. Also, that hill at mile five on that course was absolutely brutal!&lt;/p&gt;
&lt;h2&gt;The Why of It All&lt;/h2&gt;
&lt;p&gt;In looking back on this, I don't really know why I did it other than the hubris of thinking I could, and then telling someone I was going to.&lt;/p&gt;
&lt;p&gt;The hardest stretch was the runs in the second quarter. The first one (La Jolla) was the last weekend in April, the Menifee one was in the middle of May, and the Rock n Roll in San Diego was the first week of June. I ran three half marathons in five weeks. I still look back on that and think it was the point that broke me.&lt;/p&gt;
&lt;h2&gt;The Challenges&lt;/h2&gt;
&lt;p&gt;I really did want to give up more times than I can remember, but because I had said I was going to do a thing, I was going to do it no matter what.&lt;/p&gt;
&lt;p&gt;There was the nerve-wracking part where I was also going to do a run-a-mile every day between Thanksgiving and New Year's, and then on the first run, I twisted my ankle so my adventure almost didn't even start at all!&lt;/p&gt;
&lt;h2&gt;The Lessons Learned&lt;/h2&gt;
&lt;p&gt;I look back on this experience and remember that yes, I can do hard things. Yes, they are worth it, but not always for the reasons that you think they will be.&lt;/p&gt;
&lt;p&gt;Telling someone you're going to do something hard can help push you forward and keep you accountable, even just to yourself.&lt;/p&gt;
&lt;h2&gt;Looking Forward&lt;/h2&gt;
&lt;p&gt;Would I ever do something like this again? Maybe if I was 10 years younger! In all seriousness, I think I would IF I went into it with a bit of a different headspace. When I started this challenge in 2013, it was with the piss and vinegar of a young person who was sure they could conquer the world.&lt;/p&gt;
&lt;p&gt;If I did it this time, I would be more mindful. I would try to enjoy the practice of running. I would enjoy the destinations I was going to get to run in. I would share these joys more with my family.&lt;/p&gt;
&lt;h2&gt;The Support System&lt;/h2&gt;
&lt;p&gt;Doing things like this are never done alone. I think that was the thing I learned most about this. During that year, there were some sacrifices made that I didn't realize at all. My wife and daughter did a really good job of supporting me and my decision, but it did put some strain on family life.&lt;/p&gt;
&lt;h2&gt;Work-Life Balance&lt;/h2&gt;
&lt;p&gt;Something else it taught me, ironically, was a bit of work-life balance. I was the manager for a technical team at the time. We had an upgrade that was set to happen the weekend before my Menifee run in May. Due to an issue with a third-party vendor for that application, we had to push back the upgrade... I had to decide to push back the upgrade which meant it would happen the weekend of my run.&lt;/p&gt;
&lt;p&gt;As I was working towards making the decision, my boss and my team all knew what I was trying to do, and they supported not only the decision to push back the upgrade but also encouraged me to go do the run. They told me that they could handle the upgrade without me, and they did.&lt;/p&gt;
&lt;h2&gt;The Hard Lessons&lt;/h2&gt;
&lt;p&gt;That Menifee run was also where I discovered, for the first time, exactly how bad dehydration can make you feel. Not an experience I recommend, and not one I ever hope to repeat. There are still parts of Old Town Temecula that make me feel a bit queasy when I visit them!&lt;/p&gt;
&lt;h2&gt;The Final Reflection&lt;/h2&gt;
&lt;p&gt;Overall, this was a great experience, and I learned a lot about myself. But I also learned a lot about my friends, family, and coworkers as well.&lt;/p&gt;
&lt;p&gt;Would I do it again ... maybe. Would I like to run at least one of those half marathons again? Absolutely. I just need to get back into running shape!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-looking-back-at-the-half-marathon-streak-1"&gt;just take a look at my available results above via the links! &lt;a href="#sf-looking-back-at-the-half-marathon-streak-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="running"></category></entry><entry><title>Year in Review 2024</title><link href="https://ryancheley.com/2025/01/02/year-in-review-2024/" rel="alternate"></link><published>2025-01-02T00:00:00-08:00</published><updated>2025-01-02T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2025-01-02:/2025/01/02/year-in-review-2024/</id><summary type="html">&lt;p&gt;I did my first &lt;a href="https://www.ryancheley.com/2023/12/31/year-in-review-2023/"&gt;Year in Review&lt;/a&gt; last year and have decided to carry on the tradition to make sure I know what I did!&lt;/p&gt;
&lt;p&gt;I've written about themes before, so I won't go over it again here. Below is a high level of what my 2024 themes were&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/03/19/winter-of-learning/"&gt;Winter …&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;I did my first &lt;a href="https://www.ryancheley.com/2023/12/31/year-in-review-2023/"&gt;Year in Review&lt;/a&gt; last year and have decided to carry on the tradition to make sure I know what I did!&lt;/p&gt;
&lt;p&gt;I've written about themes before, so I won't go over it again here. Below is a high level of what my 2024 themes were&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/03/19/winter-of-learning/"&gt;Winter of Learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/06/20/spring-of-transition/"&gt;Spring of Transition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/10/03/summer-of-writing/"&gt;Summer of Writing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fall of Mindfulness&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall my themes were pretty successful. I do wish I had been a bit more mindful, and am now left to wonder if I shouldn't have gone with Autumn of Mindfulness instead of Fall of Mindfulness, because I seem to have taken a step back on some of my hopes for mindfulness 😄&lt;/p&gt;
&lt;h2&gt;Professional&lt;/h2&gt;
&lt;p&gt;Last year I said&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the moment it can feel like I don't really get anything done at work&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've felt this way pretty much every year for probably since I first became a manager, but I'm starting to embrace it a bit more as I get older I guess.&lt;/p&gt;
&lt;p&gt;For some context, in 2024 I worked 2235 hours with the following breakdown:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Hours&lt;/th&gt;
&lt;th&gt;Percentage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Administration&lt;/td&gt;
&lt;td&gt;1193.5&lt;/td&gt;
&lt;td&gt;53.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meetings&lt;/td&gt;
&lt;td&gt;838.1&lt;/td&gt;
&lt;td&gt;37.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding&lt;/td&gt;
&lt;td&gt;93.5&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conference&lt;/td&gt;
&lt;td&gt;55.5&lt;/td&gt;
&lt;td&gt;2.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commuting&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;2.42&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There were a couple of highlights from work this year though that I wanted to call out&lt;/p&gt;
&lt;p&gt;I celebrated 16 years with my current employer which means that my career is now old enough to drive in the US!&lt;/p&gt;
&lt;p&gt;My team finally was able to migrate our SQL Database version control from Subversion to Git. I wrote about the migration to git last year in my year in review and this was the last project that needed to be migrated over.&lt;/p&gt;
&lt;p&gt;There are still some things to do to help with the migration to make it easier for the teams that work with this project, but the first couple of steps have been completed which is nice.&lt;/p&gt;
&lt;p&gt;One of the big things I wanted to focus on was the validation issues that my company had with transmissions of claims data.&lt;/p&gt;
&lt;p&gt;Working with a couple of smart and dedicated people we were able to do some pretty amazing things.&lt;/p&gt;
&lt;p&gt;One thing to keep in mind with the validation errors is that they have to be corrected, by a person, in order to allow the claims to be transmitted to the Health Plans (which is a major goal of my company)&lt;/p&gt;
&lt;p&gt;At the start of the year, the validation error rate for Institutional Claims was 13.7% while the rate for Professional Claims was 8.7%&lt;/p&gt;
&lt;p&gt;By the end of the year those rates were down to 1.1% for Institutional Claims and 0.3% for Professional Claims. This represents decreases for 89% and 96.4% respectively.&lt;/p&gt;
&lt;p&gt;I'm really proud of what the team was able to accomplish.&lt;/p&gt;
&lt;h2&gt;Personal&lt;/h2&gt;
&lt;h3&gt;Health&lt;/h3&gt;
&lt;p&gt;I usually like to run or walk to keep my cardio health up, but I seemed to keep running into one injury or another with my knees, feet, ankles ... whatever. In July I decided to give swimming a try.&lt;/p&gt;
&lt;p&gt;Since July 15 I've swum 83,650 yards / 76489.56 meters ... which is 47.5 miles. This absolutely blows my mind because my first swim was only 200 yards, lasted about 10 minutes and I thought I was going to die.&lt;/p&gt;
&lt;p&gt;I'm now consistently swimming 3 days a week for about 55 minutes and 2000 yards / 1828.8 meters.&lt;/p&gt;
&lt;p&gt;At about the same time I really doubled down on starting a meditation practice. I tend to do about 20 minutes of meditation each day. In 2024 I had 54 hours of meditation.&lt;/p&gt;
&lt;p&gt;I wish I could split up my walking and running statistics, but Apple doesn't think these are different and so they are combined in all of the health apps! My combined Walking+Running stats came in at 1015 miles which looks like a lot, but is down significantly from my high in 2019 of nearly 2000 miles. It's also the lowest annual total by far since 2015 (my first full year of tracking)&lt;/p&gt;
&lt;p&gt;I think I know what I'll need to focus on in 2025!&lt;/p&gt;
&lt;h3&gt;Writing&lt;/h3&gt;
&lt;p&gt;In June I started a writing group with &lt;a href="https://fosstodon.org/@pythonbynight"&gt;Mario Munuz&lt;/a&gt; and &lt;a href="https://mastodon.social/@treyhunner"&gt;Trey Hunner&lt;/a&gt; and that helped to keep me motivated and accountable for writing. I didn't write nearly as much as I hoped, but I was able to get out &lt;a href="https://ryancheley.com/archive/2024/"&gt;18 articles&lt;/a&gt;. This is the most since &lt;a href="https://ryancheley.com/archive/2021/"&gt;2021&lt;/a&gt; when I wrote 23, but about 1/3 of my high mark in &lt;a href="https://ryancheley.com/archive/2018/"&gt;2018&lt;/a&gt; when I somehow was able to write 44 articles!&lt;/p&gt;
&lt;h3&gt;Open Source&lt;/h3&gt;
&lt;p&gt;This year I expanded the role I had in the Django community and I'm really pleased with that.&lt;/p&gt;
&lt;p&gt;I started the year off as a Navigator for the amazing &lt;a href="https://djangonaut.space/"&gt;Djangonaut.Space&lt;/a&gt; program in Session 1, and was able to fill that same role in Session 2.&lt;/p&gt;
&lt;p&gt;I joined the Django Commons admin group with Daniel, Lacey, Storm, and Tim. We've been able to onboard 6 libraries!&lt;/p&gt;
&lt;p&gt;I also gave a talk at Django Con US in Durham titled &lt;a href="https://www.youtube.com/watch?v=JLYaAYY4JPc"&gt;Error Culture&lt;/a&gt;. As always, my time at DjangoCon US was a blast and I'm looking forward to seeing everyone in &lt;a href="https://www.defna.org/announcements/2024/12/31/djangoconus-2025-announced/"&gt;2025 in Chicago&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I also ran for the &lt;a href="https://www.djangoproject.com/weblog/2024/dec/10/django-6x-steering-council-candidates/"&gt;Django Steering Council&lt;/a&gt;. I wasn't successful in making it into the Steering Council, but &lt;a href="https://www.djangoproject.com/weblog/2024/nov/21/announcing-the-6x-steering-council-elections/"&gt;the five folks&lt;/a&gt; that did are all amazing humans and I'm looking forward to the work that they'll do over the course of the 6.x series.&lt;/p&gt;
&lt;p&gt;I've also really enjoyed &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt;'s Office Hours. I don't do nearly enough open source work during those office hours, but it's nice to see people and listen in on, and participate in, some great conversations. I'm looking forward to doing this again in 2025&lt;/p&gt;
&lt;p&gt;I've also been trying to attend the DSF Office hours hosted by &lt;a href="https://social.jacobian.org/@jacob"&gt;Jacob Kaplan-Moss&lt;/a&gt; and &lt;a href="https://fosstodon.org/@thibaudcolas"&gt;Thibaud Colas&lt;/a&gt;. These calls are really interesting and allow a bit of a peek into the DSF Board and what's being worked on. Again, I'm excited about attending these in 2025 as well.&lt;/p&gt;
&lt;h3&gt;Sports Fandom&lt;/h3&gt;
&lt;p&gt;I post on social media a lot about Hockey. Specifically the local team near my home, the &lt;a href="https://cvfirebirds.com/"&gt;Coachella Valley Firebirds&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;They made it to the Calder Cup Finals again this year. And again they played the Hershey Bears. I &lt;a href="https://www.ryancheley.com/2023/07/01/firebirds-inaugural-season/"&gt;wrote about the 2022-23 season, and the Calder Cup finals&lt;/a&gt; and the heart break associated with losing in Overtime in Game 7 of a championship. I wish I could say that they were able to redeem themselves, but the outcome was the same ... but losing in 6 games instead of 7. That being said, it wasn't nearly as painful this time around.&lt;/p&gt;
&lt;p&gt;In all, I went to nearly 50 Firbirds Hockey games (a few on the road, but most at home) and can't really believe it. Watching Hockey live is a lot of fun!&lt;/p&gt;
&lt;p&gt;One of the highlights of the off season was running into a few of the players at a local sandwich shop and getting to chat with the captain Max McCormick. I tried to 'be cool', and I think I might have mostly succeeded, but it was a pretty surreal experience.&lt;/p&gt;
&lt;p&gt;I also had the luck to get tickets to a game at Crypto.com arena to see the LA Kings play the Seattle Kraken (which is the NHL affiliate of the Firebirds). It was an awesome game to watch due to many of the players for the Kraken being former Firebirds.&lt;/p&gt;
&lt;p&gt;The Kraken ended up losing the game 3-2 but it was still a great time.&lt;/p&gt;
&lt;p&gt;Finally, the BIG sports win this year was the Dodgers winning their first Full Season World Series since 1988. Because of life I didn't get to watch as many games of the World Series as I would have liked, but I did get to watch game 5 and that made up the missing game 1 ... I think.&lt;/p&gt;
&lt;h3&gt;Miscellaneous&lt;/h3&gt;
&lt;h4&gt;Music&lt;/h4&gt;
&lt;p&gt;I got to see a few &lt;a href="https://discoverpalmdesert.com/spring-concerts-2024/"&gt;Concerts in the Park&lt;/a&gt; which is always fun. It's free, and typically a pretty nice evening on some cool grass with a stunning view of the sunset over &lt;a href="https://en.wikipedia.org/wiki/San_Jacinto_Peak"&gt;Mt San Jacinto&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also got to see &lt;a href="https://www.ryancheley.com/2024/11/26/weezer-live/"&gt;Weezer&lt;/a&gt; and had a good time hanging out with my daughter.&lt;/p&gt;
&lt;p&gt;Finally I rediscovered the amazing music of &lt;a href="https://en.wikipedia.org/wiki/The_Tragically_Hip"&gt;The Tragically Hip&lt;/a&gt; from a &lt;a href="https://mastodon.social/@gvwilson/112666592482656620"&gt;toot&lt;/a&gt; by &lt;a href="https://mastodon.social/@gvwilson"&gt;Greg Wilson&lt;/a&gt; and it's brought me a lot of joy to listen to them again. &lt;a href="https://en.wikipedia.org/wiki/Phantom_Power_(The_Tragically_Hip_album)"&gt;Phantom Power&lt;/a&gt; is my favorite album of theirs with so many good songs. &lt;a href="https://en.wikipedia.org/wiki/Bobcaygeon_(song)"&gt;Bobcaygeon&lt;/a&gt; is probably my favorite on the album, but it can change depending on my mood.&lt;/p&gt;
&lt;h4&gt;Empty Nesting&lt;/h4&gt;
&lt;p&gt;As I wrote about &lt;a href="https://www.ryancheley.com/2024/06/20/spring-of-transition/"&gt;here&lt;/a&gt; my daughter graduated from High School and started College in the fall. This has been a big change for my wife, Emily, and I. Our daughter is pretty close by so we can visit easily, but we've tried to give her the space she needs to adjust to college life. It's been pretty successful, but it's still a weird experience to walk past her room and not see her.&lt;/p&gt;
&lt;h4&gt;Home Garden&lt;/h4&gt;
&lt;p&gt;I've been &lt;a href="https://mastodon.social/@ryancheley/113557183813506655"&gt;posting pictures of my lemon tree on Mastodon&lt;/a&gt; over the last year. In November I was finally able to harvest about 30 or 35 lemons. The great thing about a lemon tree is obviously all of the lemons. But the hard thing about all of the lemons is trying to figure out what to do with them.&lt;/p&gt;
&lt;p&gt;Emily found a great recipe for Lemon &amp;amp; Chili infused Olive Oil so we used that recipe to make about 12 bottles of our own custom olive oil and about 15 cups of Lemonade. &lt;a href="https://mastodon.social/@ryancheley/113580121231076190"&gt;These&lt;/a&gt; made some pretty amazing Christmas gifts.&lt;/p&gt;
&lt;h4&gt;Reading&lt;/h4&gt;
&lt;p&gt;Looking back at my reading for 2024 and I didn't do nearly as much as I would have liked, or think that I should have.&lt;/p&gt;
&lt;p&gt;I was able to make it about 8 Chapters into &lt;a href="https://third-bit.com/sdxpy/"&gt;Software Design by Example&lt;/a&gt;. It's a great book, but it's definitely not something you just breeze through.&lt;/p&gt;
&lt;p&gt;I was able to finish up &lt;a href="https://www.manning.com/books/practices-of-the-python-pro"&gt;Practices of the Python Pro&lt;/a&gt;. I found it to be a pretty comprehensive book. I'm not much of a book reviewer so I won't bother writing one here. I got value out of reading it, and I think others will as well.&lt;/p&gt;
&lt;p&gt;What I am really missing from my reading list for 2024 is fiction. Like any fiction at all. It doesn't look like I read anything that wasn't technical so I'll be trying to focus on fixing that in 2025&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Overall 2024 was a pretty good year for me. There were some things that I wasn't and am still not excited about, but I have decided to try and make things better where I can, stand up for what I believe is right, and just keep on trying to be kind and make the world a better place in the ways that I can.&lt;/p&gt;</content><category term="musings"></category></entry><entry><title>Weezer Live</title><link href="https://ryancheley.com/2024/11/26/weezer-live/" rel="alternate"></link><published>2024-11-26T00:00:00-08:00</published><updated>2024-11-26T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-11-26:/2024/11/26/weezer-live/</id><summary type="html">&lt;p&gt;I started college in 1996. In 1997 one of the most influential albums of my early adulthood was introduced to me ... &lt;a href="https://en.wikipedia.org/wiki/Pinkerton_(album)"&gt;Weezer's Pinkerton&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I hated it.&lt;/p&gt;
&lt;p&gt;I wanted the &lt;a href="https://en.wikipedia.org/wiki/Weezer_(Blue_Album)"&gt;Blue Album&lt;/a&gt; again but different somehow, and Pinkerton was NOT it.&lt;/p&gt;
&lt;p&gt;However, a weird thing happened. Once I moved into my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I started college in 1996. In 1997 one of the most influential albums of my early adulthood was introduced to me ... &lt;a href="https://en.wikipedia.org/wiki/Pinkerton_(album)"&gt;Weezer's Pinkerton&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I hated it.&lt;/p&gt;
&lt;p&gt;I wanted the &lt;a href="https://en.wikipedia.org/wiki/Weezer_(Blue_Album)"&gt;Blue Album&lt;/a&gt; again but different somehow, and Pinkerton was NOT it.&lt;/p&gt;
&lt;p&gt;However, a weird thing happened. Once I moved into my Sophomore apartment with a roommate that I can only describe as 'hard to live with' I retreeated into two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Final_Fantasy_VII"&gt;Final Fantasy VII&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Music&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Final Fantasy VII is a whole blog post on it's own, so I won't try to cover it here, but music ... and specifically Weezer were instrumental to me surviving my 'hard to live with' roommate.&lt;/p&gt;
&lt;h2&gt;Listening to too much Weezer&lt;/h2&gt;
&lt;p&gt;I probably listened to Pinkerton 1000 times (no exaggeration) over my time in college (this was not so hard because it's only a 35 minute album!)&lt;/p&gt;
&lt;p&gt;I loved that album, and listening to it STILL brings me back to living in a small, shitty apartment in San Luis Obispo with a 'hard to live with' roommate.&lt;/p&gt;
&lt;p&gt;I saw Weezer live in March of 2001 at &lt;a href="https://ucsdtritons.com/facilities/liontree-arena/2"&gt;RIMAC in La Jolla at UCSD&lt;/a&gt;&lt;sup id="sf-weezer-live-1-back"&gt;&lt;a href="#sf-weezer-live-1" class="simple-footnote" title="now called Liontree Arena"&gt;1&lt;/a&gt;&lt;/sup&gt;. I still remember the show. Lots of hipster Weezer fans in their skinny jeans, nerd glasses, and a lot of earnestly trying really hard trying to not try to be 'cool'. I was (still am) a bigger guy that can't fit into Skinny jeans to save my life, so this wasn't really my scene, but I really wanted to see Weezer and I didn't want to care about the hipsters.&lt;/p&gt;
&lt;p&gt;I also remember needing to be back in San Luis Obispo the next day for work or school or something. So after the show was done at midnight I got in my car and drove the roughly 4 hours back to San Luis Obsipo so I can do whatever I needed to do the 'next' day. It was a long day, but it was awesome. I got to see Weezer.&lt;/p&gt;
&lt;p&gt;And for me, that's kind of where Weezer stopped making music. A few months later Weezer released the Green Album and I tried &lt;strong&gt;really&lt;/strong&gt; hard to like it. I did that with Maladroit as well, but meh. Once Matt Sharp left it wasn't Weezer to me anymore&lt;sup id="sf-weezer-live-2-back"&gt;&lt;a href="#sf-weezer-live-2" class="simple-footnote" title="Yes, I've seen the SNL skit. Yes, I totally identify with Matt Damon's character!"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;Life has a way of happening while you're making other plans&lt;/h2&gt;
&lt;p&gt;After that life got in the way and my musical tastes changed ... but I still &lt;strong&gt;really&lt;/strong&gt; liked the "real" Weezer.&lt;/p&gt;
&lt;p&gt;One of the things I really liked doing with my daughter was driving her to her Dance class. We'd each pick an album we thought (hoped) the other would like and listen to it on the drive out.&lt;/p&gt;
&lt;p&gt;Of course, I picked Pinkerton at one point and the Blue Album and she thought they were fine. Ugh, I guess fine is better than I don't like it, but still.&lt;/p&gt;
&lt;p&gt;Fast forward to October 10th 2024 and Weezer is playing live, in an arena, not even 15 minutes from my house. I bought tickets for me and my daughter and it was pretty surreal. If you would have told me in 2001 at the Weezer concert that I'd see them again in 2024 with my adult daughter I wouldn't have believed you, but I would have thought it was a pretty cool dream to have 😄&lt;/p&gt;
&lt;p&gt;The openers were &lt;a href="https://en.wikipedia.org/wiki/Dinosaur_Jr."&gt;Dinosaur Jr.&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/The_Flaming_Lips"&gt;The Flaming Lips&lt;/a&gt;. I don't have much to say about Dinosaur Jr, but the Flaming Lips put on a great show. Lots of visual interest and excitement.&lt;/p&gt;
&lt;p&gt;Then Weezer came on. And the show was amazing. Again, lots of great visuals and set design. One of my favorite was 'Island in the Sun' which had a Giant star in the background of a tropical looking island with a palm tree on it.&lt;/p&gt;
&lt;p&gt;Again, the set design and visual aspects of the show were on point.&lt;/p&gt;
&lt;p&gt;The only thing that wasn't great was the sound. If I listed to Pinkerton 1000 times, I listened to the Blue album at least 500 times. And the concert was meant to be a 30th anniversary of the Blue Album where they played it in order. And I have to say, for the first 5 - 10 seconds of each song I wasn't sure what song was being played ... it was a bit disappointing.&lt;/p&gt;
&lt;p&gt;That being said, getting to see Weezer, with my daughter, was a pretty epic parenting level unlocked style achievement. I'm glad I got to go with her, even if the sounds was a bit muffled for my tastes.&lt;/p&gt;
&lt;p&gt;Looking forward to the next concert!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-weezer-live-1"&gt;now called Liontree Arena &lt;a href="#sf-weezer-live-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-weezer-live-2"&gt;Yes, I've seen &lt;a href="https://www.youtube.com/watch?v=ab5WvwfLuLM"&gt;the SNL skit&lt;/a&gt;. Yes, I totally identify with Matt Damon's character! &lt;a href="#sf-weezer-live-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="music"></category></entry><entry><title>uv and pip</title><link href="https://ryancheley.com/2024/11/23/uv-and-pip/" rel="alternate"></link><published>2024-11-23T00:00:00-08:00</published><updated>2024-11-23T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-11-23:/2024/11/23/uv-and-pip/</id><summary type="html">&lt;p&gt;On Sunday November 3 I posted &lt;a href="https://mastodon.social/@ryancheley/113420509533590631"&gt;this&lt;/a&gt; to Mastodon:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I've somehow managed to get Python on my macbook to not install packages into the virtual environment I've activated and I'm honestly not sure how to fix this.&lt;/p&gt;
&lt;p&gt;Has anyone else ever run into this problem? If so, any pointers on …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;On Sunday November 3 I posted &lt;a href="https://mastodon.social/@ryancheley/113420509533590631"&gt;this&lt;/a&gt; to Mastodon:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I've somehow managed to get Python on my macbook to not install packages into the virtual environment I've activated and I'm honestly not sure how to fix this.&lt;/p&gt;
&lt;p&gt;Has anyone else ever run into this problem? If so, any pointers on how to fix it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I got lots of helpful replies and with those replies I was able to determine what the issue was and 'fix' it.&lt;/p&gt;
&lt;h2&gt;A timeline of events&lt;/h2&gt;
&lt;p&gt;I was working on updating a &lt;a href="https://github.com/ryancheley/the-well-maintained-test"&gt;library of mine&lt;/a&gt; and because it had been a while since it had been worked on, I had to git clone it locally. When I did that I then set out to try &lt;code&gt;uv&lt;/code&gt; for the virtual environment management.&lt;/p&gt;
&lt;p&gt;This worked well (and was lightning FAST) and I was hacking away at the update I wanted to do.&lt;/p&gt;
&lt;p&gt;Then I had a call with my daughter to review her upcoming schedule for the spring semester. When I got back to working on my library I kind of dove right in and started to get an error messages about the library not being installed&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;zsh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;well&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;maintained&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So I tried to install it (though I was 100% sure it was already there) and got this message&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Could&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;activated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;virtualenv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I deleted the venv directory and started over again (using &lt;code&gt;uv&lt;/code&gt; still) and ran into the same issue.&lt;/p&gt;
&lt;p&gt;I restarted my Mac (at my day job I use Windows computers and this is just a natural reaction to do when something doesn't work the way I think it should&lt;sup id="sf-uv-and-pip-1-back"&gt;&lt;a href="#sf-uv-and-pip-1" class="simple-footnote" title="Yes this is dumb, and yes I hate it"&gt;1&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;
&lt;p&gt;That didn't fix the issue 😢&lt;/p&gt;
&lt;p&gt;I spent the next little while certain that in some way &lt;code&gt;pipx&lt;/code&gt; or &lt;code&gt;pyenv&lt;/code&gt; had jacked up my system, so I uninstalled them ... now you might ask &lt;em&gt;why&lt;/em&gt; I thought this, and dear reader, I have no f$%&amp;amp;ing clue.&lt;/p&gt;
&lt;p&gt;With those &lt;em&gt;pesky&lt;/em&gt; helpers out of the way, &lt;code&gt;pip&lt;/code&gt; still wasn't working the way I expected it to!&lt;/p&gt;
&lt;p&gt;I then took to Mastodon and with this &lt;a href="https://fosstodon.org/@browniebroke/113420548462836451"&gt;one response&lt;/a&gt; I saw what I needed&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;@ryancheley Are you running python -m pip install... Or just pip install...? If that's a venv created by uv, pip isn't installed I think, so 'pip install' might resolve to a pip in a different python installation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I went back to my terminal, and sure enough that was the issue. I haven't used &lt;code&gt;uv&lt;/code&gt; enough to get a real sense of it, and when I was done talking with my daughter, my brain switched to Python programming, but it forgot that I had used &lt;code&gt;uv&lt;/code&gt; to set everything up.&lt;/p&gt;
&lt;h2&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;This was a good lesson but I'm still unsure about a few things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How do I develop a cli using &lt;code&gt;uv&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Why did it seem that my cli testing worked fine right up until the call with my daughter, and now it seems that I can't develop cli's with &lt;code&gt;uv&lt;/code&gt;?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I did write a &lt;a href="https://github.com/ryancheley/til/blob/main/uv/uv-venv.md"&gt;TIL&lt;/a&gt; for this but I discovered that&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uv&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;is not a full replacement for&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Specifically &lt;code&gt;uv&lt;/code&gt; does not include &lt;code&gt;pip&lt;/code&gt;, which is what contributed to my issues. You &lt;strong&gt;can&lt;/strong&gt; include &lt;code&gt;pip&lt;/code&gt; by running this command though&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uv&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;--seed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Needless to say, with the help of some great people on the internet I got my issue resolved, but I did spend a good portion of Monday evening un-f$%&amp;amp;ing my MacBook Pro by reinstalling pyenv, and pipx&lt;sup id="sf-uv-and-pip-2-back"&gt;&lt;a href="#sf-uv-and-pip-2" class="simple-footnote" title="As of this writing I've uninstalled pipx because uv can replace it too. See Jeff Triplett's post uv does everything"&gt;2&lt;/a&gt;&lt;/sup&gt; ... and cleaning up my system Python for 3.12 and 3.13 ... turns out Homebrew REALLY doesn't want you to do anything with the system Python, even if you accidentally installed a bunch of cruft in there accidentally.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-uv-and-pip-1"&gt;Yes this is dumb, and yes I hate it &lt;a href="#sf-uv-and-pip-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-uv-and-pip-2"&gt;As of this writing I've uninstalled pipx because &lt;code&gt;uv&lt;/code&gt; can replace it too. See Jeff Triplett's post &lt;a href="https://micro.webology.dev/2024/11/03/uv-does-everything.html"&gt;uv does everything&lt;/a&gt; &lt;a href="#sf-uv-and-pip-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="pip"></category><category term="python"></category><category term="uv"></category></entry><entry><title>Migrating django-tailwind-cli to Django Commons</title><link href="https://ryancheley.com/2024/11/20/migrating-django-tailwind-cli-to-django-commons/" rel="alternate"></link><published>2024-11-20T00:00:00-08:00</published><updated>2024-11-20T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-11-20:/2024/11/20/migrating-django-tailwind-cli-to-django-commons/</id><summary type="html">&lt;p&gt;On Tuesday October 29 I worked with &lt;a href="https://github.com/oliverandrich/"&gt;Oliver Andrich&lt;/a&gt;, &lt;a href="https://github.com/cunla/"&gt;Daniel Moran&lt;/a&gt; and &lt;a href="https://github.com/Stormheg"&gt;Storm Heg&lt;/a&gt; to migrate Oliver's project &lt;a href="https://github.com/django-commons/django-tailwind-cli"&gt;django-tailwind-cli&lt;/a&gt; from Oliver's GitHub project to Django Commons.&lt;/p&gt;
&lt;p&gt;This was the 5th library that has been migrated over, but the first one that I 'lead'. I was a bit nervous. The Django …&lt;/p&gt;</summary><content type="html">&lt;p&gt;On Tuesday October 29 I worked with &lt;a href="https://github.com/oliverandrich/"&gt;Oliver Andrich&lt;/a&gt;, &lt;a href="https://github.com/cunla/"&gt;Daniel Moran&lt;/a&gt; and &lt;a href="https://github.com/Stormheg"&gt;Storm Heg&lt;/a&gt; to migrate Oliver's project &lt;a href="https://github.com/django-commons/django-tailwind-cli"&gt;django-tailwind-cli&lt;/a&gt; from Oliver's GitHub project to Django Commons.&lt;/p&gt;
&lt;p&gt;This was the 5th library that has been migrated over, but the first one that I 'lead'. I was a bit nervous. The Django Commons docs are great and super helpful, but the first time you do something, it can be nerve wracking.&lt;/p&gt;
&lt;p&gt;One thing that was super helpful was knowing that Daniel and Storm were there to help me out when any issues came up.&lt;/p&gt;
&lt;p&gt;The first set up steps are pretty straight forward and we were able to get through them pretty quickly. Then we ran into an issue that none of us had seen previously.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;django-tailwind-cli&lt;/code&gt; had initially set up GitHub Pages set up for the docs, but migrated to use &lt;a href="https://about.readthedocs.com/"&gt;Read the Docs&lt;/a&gt;. However, the GitHub pages were still set in the repo so when we tried to migrate them over we ran into an error. Apparently you can't remove GitHub pages using Terraform (the process that we use to manage the organization).&lt;/p&gt;
&lt;p&gt;We spent a few minutes trying to parse the error, make some changes, and try again (and again) and we were able to finally successfully get the migration completed 🎉&lt;/p&gt;
&lt;p&gt;Some other things that came up during the migration was a maintainer that was set in the front end, but not in the terraform file. Also, while I was making changes to the Terraform file locally I ran into an issue with an update that had been done in the GitHub UI on my branch which caused a conflict for me locally.&lt;/p&gt;
&lt;p&gt;I've had to deal with this kind of thing before, but ... never with an audience! Trying to work through the issue was a bit stressful to say the least 😅&lt;/p&gt;
&lt;p&gt;But, with the help of Daniel and Storm I was able to resolve the conflicts and get the code pushed up.&lt;/p&gt;
&lt;p&gt;As of this writing we have &lt;a href="https://github.com/orgs/django-commons/repositories?type=source&amp;amp;q=language%3APython+-topic%3Atemplate"&gt;6 libraries&lt;/a&gt; that are part of the Django Commons organization and am really excited for the next time that I get to lead a migration. Who knows, at some point I might actually be able to do one on my own ... although our hope is that this can be automated much more ... so maybe that's what I can work on next&lt;/p&gt;
&lt;p&gt;Working on a project like this has been really great. There are such great opportunities to learn various technologies (terraform, GitHub Actions, git) and getting to work with great collaborators.&lt;/p&gt;
&lt;p&gt;What I'm hoping to be able to work on this coming weekend is&lt;sup id="sf-migrating-django-tailwind-cli-to-django-commons-1-back"&gt;&lt;a href="#sf-migrating-django-tailwind-cli-to-django-commons-1" class="simple-footnote" title="Now will I actually be able to 🤷🏻"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get a better understanding of Terraform and how to use it with GitHub&lt;/li&gt;
&lt;li&gt;Use Terraform to do something with GitHub Actions&lt;/li&gt;
&lt;li&gt;Try and create a merge conflict and then use the git cli, or Git Tower, or VS Code to resolve the merge conflict&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For number 3 in particular I want to have more comfort for fixing those kinds of issues so that if / when they come up again I can resolve them.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-migrating-django-tailwind-cli-to-django-commons-1"&gt;Now will I actually be able to 🤷🏻 &lt;a href="#sf-migrating-django-tailwind-cli-to-django-commons-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="django"></category><category term="oss"></category><category term="django-commons"></category></entry><entry><title>DjangoCon US 2024</title><link href="https://ryancheley.com/2024/11/17/djangocon-us-2024/" rel="alternate"></link><published>2024-11-17T00:00:00-08:00</published><updated>2024-11-17T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-11-17:/2024/11/17/djangocon-us-2024/</id><summary type="html">&lt;h1&gt;DjangoCon US 2024&lt;/h1&gt;
&lt;p&gt;I was able to attend &lt;a href="https://2024.djangocon.us"&gt;DCUS 2024&lt;/a&gt; this year in Durham from September 22 - September 27, and just like in 2023, it was an amazing experience.&lt;/p&gt;
&lt;p&gt;I gave another &lt;a href="https://www.youtube.com/watch?v=JLYaAYY4JPc"&gt;talk&lt;/a&gt; (hooray!) and got to hang out with some truly amazing people, many of whom I call my …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;DjangoCon US 2024&lt;/h1&gt;
&lt;p&gt;I was able to attend &lt;a href="https://2024.djangocon.us"&gt;DCUS 2024&lt;/a&gt; this year in Durham from September 22 - September 27, and just like in 2023, it was an amazing experience.&lt;/p&gt;
&lt;p&gt;I gave another &lt;a href="https://www.youtube.com/watch?v=JLYaAYY4JPc"&gt;talk&lt;/a&gt; (hooray!) and got to hang out with some truly amazing people, many of whom I call my friends.&lt;/p&gt;
&lt;p&gt;I was fortunate in that my talk was on Monday morning, so as soon as my talk was done, I could focus on the conference and less on being nervous about my talk!&lt;/p&gt;
&lt;p&gt;One thing I took advantage of this year, that I didn't in previous years, was the 'Hallway Track'. I really enjoyed that time on Monday afternoon to decompress with some of the other speakers in the lobby.&lt;/p&gt;
&lt;p&gt;One of the talks that I was able to watch since the conference was &lt;a href="https://www.youtube.com/watch?v=a7iUKbug82k"&gt;Troubleshooting is a Lifestyle 😎&lt;/a&gt; which had this great note: Asking for help is not a sign of failure - it's a strategy.&lt;/p&gt;
&lt;p&gt;I am bummed that I missed a few talks live (&lt;a href="https://www.youtube.com/watch?v=75M0MC66H2o"&gt;Product 101 for Techies and Tech Teams&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=ylv_k8TRpPk"&gt;Passkeys: Your password-free future&lt;/a&gt;, and &lt;a href="https://www.youtube.com/watch?v=X0Urp3RsKLY"&gt;Django: the web framework that changed my life&lt;/a&gt;) but I will go back and watch them in the next several days and I'm really looking forward to that.&lt;/p&gt;
&lt;p&gt;There is a great &lt;a href="https://www.youtube.com/playlist?list=PL2NFhrDSOxgWqE_5w5CX2iUR7-P1D0ny7"&gt;playlist&lt;/a&gt; of ALL of the talks from this year (and previous years) that I highly recommend you search through and watch!&lt;/p&gt;
&lt;p&gt;A few others have written about their experiences (&lt;a href="https://pythonbynight.com/blog/djangocon-2024"&gt;Mario Munoz&lt;/a&gt; and &lt;a href="https://wsvincent.com/djangoconus-recap/"&gt;Will Vincent&lt;/a&gt;) and you should totally read those. Some of the&lt;/p&gt;
&lt;h2&gt;The Food&lt;/h2&gt;
&lt;p&gt;DCUS via the culinary experience!&lt;/p&gt;
&lt;p&gt;Durham has some of the best food and I would go back again JUST for the food. Some of my highlights were&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cheenidurham.com/"&gt;Cheeni&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.thaiangleofdurham.com/"&gt;Thaiangle of Durham&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.queenysdurham.com/"&gt;Queeny's&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ponysaurusbrewing.com/"&gt;Ponysaurus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://littlewaves.coffee/pages/old-north-durham?srsltid=AfmBOooaYRO5ZB5bS9mZ43O1J_lVMyXSD_4ma0i8GZjaRg7UxcOgPaAm"&gt;Cocoa Cinnamon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pizzeriatoro.com/"&gt;Pizza Torro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The conference venue food - fried chicken and peach cobbler were my favorite&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Sprints&lt;/h2&gt;
&lt;p&gt;During the sprints I was able to work on a few tickets for DjangoPackages&lt;sup id="sf-djangocon-us-2024-1-back"&gt;&lt;a href="#sf-djangocon-us-2024-1" class="simple-footnote" title="settings consolidaion"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;sup id="sf-djangocon-us-2024-2-back"&gt;&lt;a href="#sf-djangocon-us-2024-2" class="simple-footnote" title="docs update"&gt;2&lt;/a&gt;&lt;/sup&gt; and get some clarification on a Django doc&lt;sup id="sf-djangocon-us-2024-3-back"&gt;&lt;a href="#sf-djangocon-us-2024-3" class="simple-footnote" title="27106"&gt;3&lt;/a&gt;&lt;/sup&gt; ticket that's I've been wanting to work on for a while now.&lt;/p&gt;
&lt;h2&gt;The after party in Palm Springs&lt;/h2&gt;
&lt;p&gt;I left Durham &lt;em&gt;very&lt;/em&gt; early on Saturday morning to head back home to Southern California. Leaving a great conference like DjangoCon US can be hard as Kojo &lt;a href="https://kojoidrissa.com/conferences/community/pycon%20africa/noramgt/2019/08/11/post_conference_depression.html"&gt;has written about&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One upside for me was knowing that a few people from the conference were road tripping out to California and they were going to stop and visit! The following week I had a great dinner with Thibaud, Sage, and Storm at &lt;a href="https://tacquila.com/"&gt;Tac/Quila&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://mastodon.social/@ryancheley/113237643354514479"&gt;a toot on Mastodon&lt;/a&gt; with a picture of the 4 of us after dinner&lt;/p&gt;
&lt;h2&gt;Looking Forward&lt;/h2&gt;
&lt;p&gt;I just feel so much more clam after the conference, and am super happy.&lt;/p&gt;
&lt;p&gt;I'm looking forward to my involvement in the Django Community until the next DjangoCon I'm able to attend&lt;sup id="sf-djangocon-us-2024-4-back"&gt;&lt;a href="#sf-djangocon-us-2024-4" class="simple-footnote" title="I'm working really hard on DCEU but the timing may not work out"&gt;4&lt;/a&gt;&lt;/sup&gt;. Some things specifically are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Working on Django tickets&lt;/li&gt;
&lt;li&gt;Admin work with Django Commons with Tim, Lacey, Daniel, and Storm&lt;/li&gt;
&lt;li&gt;Working on Django Packages with Jeff and Maksudul&lt;/li&gt;
&lt;li&gt;Djangonaut Space (if and when they need a navigator but just hanging out in the discord is pretty awesome too!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm so grateful for the friends and community that Django has given to me. I'm really hoping to be able to pay it forward with my involvement over the next year until I have a chance to see all of these amazing people in person again&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-djangocon-us-2024-1"&gt;settings consolidaion &lt;a href="#sf-djangocon-us-2024-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-djangocon-us-2024-2"&gt;docs update &lt;a href="#sf-djangocon-us-2024-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-djangocon-us-2024-3"&gt;27106 &lt;a href="#sf-djangocon-us-2024-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-djangocon-us-2024-4"&gt;I'm working really hard on DCEU but the timing may not work out &lt;a href="#sf-djangocon-us-2024-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="django"></category><category term="community"></category></entry><entry><title>Django Commons</title><link href="https://ryancheley.com/2024/10/23/django-commons/" rel="alternate"></link><published>2024-10-23T00:00:00-07:00</published><updated>2024-10-23T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-10-23:/2024/10/23/django-commons/</id><summary type="html">&lt;p&gt;First, what are "the commons"? The concept of "the commons" refers to resources that are shared and managed collectively by a community, rather than being owned privately or by the state. This idea has been applied to natural resources like air, water, and grazing land, but it has also expanded …&lt;/p&gt;</summary><content type="html">&lt;p&gt;First, what are "the commons"? The concept of "the commons" refers to resources that are shared and managed collectively by a community, rather than being owned privately or by the state. This idea has been applied to natural resources like air, water, and grazing land, but it has also expanded to include digital and cultural resources, such as open-source software, knowledge databases, and creative works.&lt;/p&gt;
&lt;p&gt;As Organization Administrators of Django Commons, we're focusing on sustainability and stewardship as key aspects.&lt;/p&gt;
&lt;p&gt;Asking for help is hard, but it can be done more easily in a safe environment. As we saw with the &lt;a href="https://en.wikipedia.org/wiki/XZ_Utils_backdoor"&gt;xz utils backdoor&lt;/a&gt; attack, maintainer burnout is real. And while there are several arguments about being part of a 'supply chain' if we can, as a community, offer up a place where maintainers can work together for the sustainability and support of their packages, Django community will be better off!&lt;/p&gt;
&lt;p&gt;From the &lt;a href="https://github.com/django-commons/membership/blob/main/README.md"&gt;README&lt;/a&gt; of the membership repo in Django Commons&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Django Commons is an organization dedicated to supporting the community's efforts to maintain packages. It seeks to improve the maintenance experience for all contributors; reducing the barrier to entry for new contributors and reducing overhead for existing maintainers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK, but what does this new organization get me as a maintainer? The (stretch) goal is that we'll be able to provide support to maintainers. Whether that's helping to identify best practices for packages (like requiring tests), or normalize the idea that maintainers can take a step back from their project and know that there will be others to help keep the project going. Being able to accomplish these two goals would be amazing ... but we want to do more!&lt;/p&gt;
&lt;p&gt;In the long term we're hoping that we're able to do something to help provide compensation to maintainers, but as I said, that's a long term goal.&lt;/p&gt;
&lt;p&gt;The project was spearheaded by Tim Schilling and he was able to get lots of interest from various folks in the Django Community. But I think one of the great aspects of this community project is the transparency that we're striving for. You can see &lt;a href="https://github.com/orgs/django-commons/discussions/19"&gt;here&lt;/a&gt; an example of a discussion, out in the open, as we try to define what we're doing, together. Also, while Tim spearheaded this effort, we're really all working as equals towards a common goal.&lt;/p&gt;
&lt;p&gt;What we're building here is a sustainable infrastructure and community. This community will allow packages to have a good home, to allow people to be as active as they want to be, and also allow people to take a step back when they need to.&lt;/p&gt;
&lt;p&gt;Too often in tech, and especially in OSS, maintainers / developers will work and work and work because the work they do is generally interesting, and has interesting problems to try and solve.&lt;/p&gt;
&lt;p&gt;But this can have a downside that we've all seen .. burnout.&lt;/p&gt;
&lt;p&gt;By providing a platform for maintainers to 'park' their projects, along with the necessary infrastructure to keep them active, the goal is to allow maintainers the opportunity to take a break if, or when, they need to. When they're ready to return, they can do so with renewed interest, with new contributors and maintainers who have helped create a more sustainable environment for the open-source project.&lt;/p&gt;
&lt;p&gt;The idea for this project is very similar to, but different from, Jazz Band. Again, from the &lt;a href="https://github.com/django-commons/membership/blob/main/README.md"&gt;README&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Django Commons and Jazzband have similar goals, to support community-maintained projects. There are two main differences. The first is that Django Commons leans into the GitHub paradigm and centers the organization as a whole within GitHub. This is a risk, given there's some vendor lock-in. However, the repositories are still cloned to several people's machines and the organization controls the keys to PyPI, not GitHub. If something were to occur, it's manageable.&lt;/p&gt;
&lt;p&gt;The second is that Django Commons is built from the beginning to have more than one administrator. Jazzband has been &lt;a href="https://github.com/jazzband/help/issues/196"&gt;working for a while to add additional roadies&lt;/a&gt; (administrators), but there hasn't been visible progress. Given the importance of several of these projects it's a major risk to the community at large to have a single point of failure in managing the projects. By being designed from the start to spread the responsibility, it becomes easier to allow people to step back and others to step up, making Django more sustainable and the community stronger.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One of the goals for Django Commons is to be very public about what's going on. We actively encourage use of the &lt;a href="https://github.com/orgs/django-commons/discussions"&gt;Discussions&lt;/a&gt; feature in GitHub and have several active conversations happening there now&lt;sup id="sf-django-commons-1-back"&gt;&lt;a href="#sf-django-commons-1" class="simple-footnote" title="How to approach existing libraries"&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;sup id="sf-django-commons-2-back"&gt;&lt;a href="#sf-django-commons-2" class="simple-footnote" title="Creating a maintainer-contributor feedback loop"&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;sup id="sf-django-commons-3-back"&gt;&lt;a href="#sf-django-commons-3" class="simple-footnote" title="DjangoCon US 2024 Maintainership Open pace"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So far we've been able to migrate ~3~ 4 libraries&lt;sup id="sf-django-commons-4-back"&gt;&lt;a href="#sf-django-commons-4" class="simple-footnote" title="django-tasks-scheduler"&gt;4&lt;/a&gt;&lt;/sup&gt; &lt;sup id="sf-django-commons-5-back"&gt;&lt;a href="#sf-django-commons-5" class="simple-footnote" title="django-typer"&gt;5&lt;/a&gt;&lt;/sup&gt; &lt;sup id="sf-django-commons-6-back"&gt;&lt;a href="#sf-django-commons-6" class="simple-footnote" title="django-fsm-2"&gt;6&lt;/a&gt;&lt;/sup&gt; &lt;sup id="sf-django-commons-7-back"&gt;&lt;a href="#sf-django-commons-7" class="simple-footnote" title="django-debug-toolbar"&gt;7&lt;/a&gt;&lt;/sup&gt;into Django Commons. Each one has been a great learning experience, not only for the library maintainers, but also for the Django Commons admins.&lt;/p&gt;
&lt;p&gt;We're working to automate as much of the work as possible. &lt;a href="https://github.com/cunla/"&gt;Daniel Moran&lt;/a&gt; has done an amazing job of writing Terraform scripts to help in the automation process.&lt;/p&gt;
&lt;p&gt;While there are still several manual steps, with each new library, we discover new opportunities for automation.&lt;/p&gt;
&lt;p&gt;This is an exciting project to be a part of. If you're interested in joining us you have a couple of options&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/django-commons/membership/issues/new?assignees=django-commons%2Fadmins&amp;amp;labels=Transfer+project+in&amp;amp;projects=&amp;amp;template=transfer-project-in.yml&amp;amp;title=%F0%9F%9B%AC+%5BINBOUND%5D+-+%3Cproject%3E"&gt;Transfer your project&lt;/a&gt; into Django Commons&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/django-commons/membership/issues/new?assignees=django-commons%2Fadmins&amp;amp;labels=New+member&amp;amp;projects=&amp;amp;template=new-member.yml&amp;amp;title=%E2%9C%8B+%5BMEMBER%5D+-+%3Cyour+handle%3E"&gt;Join as member&lt;/a&gt; and help contribute to one of the projects that's already in Django Commons&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I'm looking forward to seeing you be part of this amazing community!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-django-commons-1"&gt;&lt;a href="https://github.com/orgs/django-commons/discussions/52"&gt;How to approach existing libraries&lt;/a&gt; &lt;a href="#sf-django-commons-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-2"&gt;&lt;a href="https://github.com/orgs/django-commons/discussions/61"&gt;Creating a maintainer-contributor feedback loop&lt;/a&gt; &lt;a href="#sf-django-commons-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-3"&gt;&lt;a href="https://github.com/orgs/django-commons/discussions/42"&gt;DjangoCon US 2024 Maintainership Open pace&lt;/a&gt; &lt;a href="#sf-django-commons-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-4"&gt;&lt;a href="https://github.com/django-commons/django-tasks-scheduler"&gt;django-tasks-scheduler&lt;/a&gt; &lt;a href="#sf-django-commons-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-5"&gt;&lt;a href="https://github.com/django-commons/django-typer"&gt;django-typer&lt;/a&gt; &lt;a href="#sf-django-commons-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-6"&gt;&lt;a href="https://github.com/django-commons/django-fsm-2"&gt;django-fsm-2&lt;/a&gt; &lt;a href="#sf-django-commons-6-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-django-commons-7"&gt;&lt;a href="https://github.com/django-commons/django-debug-toolbar/"&gt;django-debug-toolbar&lt;/a&gt; &lt;a href="#sf-django-commons-7-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="django"></category><category term="oss"></category><category term="django-commons"></category></entry><entry><title>DjangoCon US 2024 Talk</title><link href="https://ryancheley.com/2024/10/17/djangocon-us-2024-talk/" rel="alternate"></link><published>2024-10-17T00:00:00-07:00</published><updated>2024-10-17T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-10-17:/2024/10/17/djangocon-us-2024-talk/</id><summary type="html">&lt;p&gt;At DjangoCon US 2023 I gave a talk, and wrote about my experience &lt;a href="https://www.ryancheley.com/2023/12/15/so-you-want-to-give-a-talk-at-a-conference/"&gt;preparing for that talk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, I spoke again at DjangoCon US this year (2024) and had a similar, but wildly different experience in preparing for my talk.&lt;/p&gt;
&lt;p&gt;Last year I lamented that I didn't really track my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At DjangoCon US 2023 I gave a talk, and wrote about my experience &lt;a href="https://www.ryancheley.com/2023/12/15/so-you-want-to-give-a-talk-at-a-conference/"&gt;preparing for that talk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, I spoke again at DjangoCon US this year (2024) and had a similar, but wildly different experience in preparing for my talk.&lt;/p&gt;
&lt;p&gt;Last year I lamented that I didn't really track my time (which is weird because I track my time for ALL sorts of things!).&lt;/p&gt;
&lt;p&gt;This year, I did track my time and have a much better sense of how much time I prepared for the talk.&lt;/p&gt;
&lt;p&gt;Another difference between each year is that in 2023 I gave a 45 minute talk, while this year my talk was 25 minutes.&lt;/p&gt;
&lt;p&gt;I've heard that you need about 1 hour of prep time for each 1 minute of talk that you're going to give. That means that, on average, for a 25 minute talk I'd need about 25 hours of prep time.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://track.toggl.com/shared-report/6c52f45a0feea26f7c8fd987abf73b2e"&gt;My time tracking shows&lt;/a&gt; that I was a little short of that (19 hours) but my talk ended up being about 20 minutes, so it seems that maybe I was on track for that.&lt;/p&gt;
&lt;p&gt;This year, as last year, my general prep technique was to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Give the presentation AND record it&lt;/li&gt;
&lt;li&gt;Watch the recording and make notes about what I needed to change&lt;/li&gt;
&lt;li&gt;Make the changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I would typically do each step on a different day, though towards the end I would do steps 2 and 3 on the same day, and during the last week I would do all of the steps on the same day.&lt;/p&gt;
&lt;p&gt;This flow really seems to help me get the most of out practicing my talk and getting a sense of its strengths and weaknesses.&lt;/p&gt;
&lt;p&gt;One issue that came up a week before I was to leave for DjangoCon US is that my boss said I couldn't have anything directly related to my employer in the presentation. My initial drafts didn't have specifics, but the examples I used were too close for my comfort on that, so I ended up having to refactor that part of my talk.&lt;/p&gt;
&lt;p&gt;Honestly, I think it came out better because of it. During my practice runs I felt like I was kind of dancing around topics, but once I removed them i felt freer to just kind of speak my mind.&lt;/p&gt;
&lt;p&gt;Preparing and giving talks like these are truly a ton of work. Yes, you'll (most likely) be given a free ticket to the conference you're speaking at — but unless you're a seasoned public speaker you will have to practice a lot to give a great talk.&lt;/p&gt;
&lt;p&gt;One thing I didn't mention in my prep time is that my talk was essentially just a rendition of my series of blog posts I started writing at DjangoCon US 2023 (&lt;a href="https://www.ryancheley.com/2023/10/29/error-culture/"&gt;Error Culture&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;So when you add in the time it took for me to brainstorm those articles, write, and edit them, we're probably looking at another 5 - 7 hours of prep.&lt;/p&gt;
&lt;p&gt;This puts me closer to the 25 hours of prep time for the 25 minute talk.&lt;/p&gt;
&lt;p&gt;I've given 2 talks so far, and after each one I've said, 'Never again!'&lt;/p&gt;
&lt;p&gt;It's been a few weeks since I gave my talk, and I have to say, I'm kind of looking forward to trying to give a talk again next year. Now, I just need to figure out what I would talk about that anyone would want to hear. 🤔&lt;/p&gt;</content><category term="technology"></category><category term="dcus"></category><category term="conference-talks"></category></entry><entry><title>Summer of Writing</title><link href="https://ryancheley.com/2024/10/03/summer-of-writing/" rel="alternate"></link><published>2024-10-03T00:00:00-07:00</published><updated>2024-10-03T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-10-03:/2024/10/03/summer-of-writing/</id><summary type="html">&lt;p&gt;In keeping with my themes for 2024 this summer was to be 'The Summer of Writing'.&lt;/p&gt;
&lt;p&gt;This theme didn't have a specific post or word count, but I knew I wanted to write &lt;strong&gt;more&lt;/strong&gt;&lt;sup id="sf-summer-of-writing-1-back"&gt;&lt;a href="#sf-summer-of-writing-1" class="simple-footnote" title="How much more? I don't really know ... just more"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I had a few things I needed to do to get this started. One of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In keeping with my themes for 2024 this summer was to be 'The Summer of Writing'.&lt;/p&gt;
&lt;p&gt;This theme didn't have a specific post or word count, but I knew I wanted to write &lt;strong&gt;more&lt;/strong&gt;&lt;sup id="sf-summer-of-writing-1-back"&gt;&lt;a href="#sf-summer-of-writing-1" class="simple-footnote" title="How much more? I don't really know ... just more"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I had a few things I needed to do to get this started. One of them included starting a writing cohort. I wasn't sure how I would do that but it turns out in early June &lt;a href="https://hachyderm.io/@lacey"&gt;Lacey&lt;/a&gt; was &lt;a href="https://hachyderm.io/@lacey/112550334978638433"&gt;having similar thoughts&lt;/a&gt;. &lt;a href="https://hachyderm.io/@pythonbynight@fosstodon.org"&gt;Mario&lt;/a&gt; and &lt;a href="https://hachyderm.io/@treyhunner@mastodon.social"&gt;Trey&lt;/a&gt; had some interest as well and so we formed a writing group!&lt;/p&gt;
&lt;p&gt;We meet every Wednesday (more or less) for about an hour.&lt;/p&gt;
&lt;p&gt;I had really hoped that the forming the cohort would give me the encouragement and accountability I needed ... and it HAS!&lt;/p&gt;
&lt;p&gt;But I also quickly realized that all I had calendared (really) was the Wednesday writing session, and so I set out to have a 30 minute daily writing session on my own.&lt;/p&gt;
&lt;p&gt;Life has a way of kicking you in the ass though when you least expect it.&lt;/p&gt;
&lt;p&gt;Roughly 10 days into my 'Summer of Writing' a work thing came up and kind of consumed all of my thought and energy. I realized quickly that something had to give, and so I looked ahead at my Autumn theme and borrowed from it a bit, while still keeping the spirit of trying to write.&lt;/p&gt;
&lt;p&gt;The next theme was going to be 'The Autumn of Mindfulness' which included starting a meditation practice so I dove into that. I also decided that I needed to try to find something to do from a physical activity perspective. I live in the desert of southern california so the summers are brutal ( daily highs that can average 110F+) and being outside isn't something I really like, even in the early morning, before the sunrise, the temps can be mid to high 80s ... sometimes even the low 90s.&lt;/p&gt;
&lt;p&gt;I decided that I would pick up swimming and going to the gym to help alleviate some of the stress from work.&lt;/p&gt;
&lt;p&gt;That, in addition to the writing, seemed to be a good thing to work on.&lt;/p&gt;
&lt;p&gt;During my Summer of Writing I only wrote 5 posts (including this one)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/10/03/summer-of-writing/"&gt;Summer of Writing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/07/13/ssh-keys/"&gt;SSH Keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/08/22/how-to-ask-why-without-sounding-like-a-jerk/"&gt;How to ask a question without sounding like a jerk&lt;/a&gt; (mentioned in the &lt;a href="https://django-news.com/issues/248"&gt;Django News Newsletter #248&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/08/29/reflections-on-djangonaut-space-session-2/"&gt;Reflections on Djangonaut Space Session 2&lt;/a&gt; (&lt;a href="https://mastodon.social/@ryancheley/113044831659253804"&gt;lots of love&lt;/a&gt; on socials)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/09/16/mentors/"&gt;Mentors&lt;/a&gt; (mentioned in &lt;a href="https://django-news.com/issues/251#start"&gt;Django News Newsletter #251&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The articles had a total of 5237 words and so, from the perspective of writing, I managed to do &lt;em&gt;some&lt;/em&gt; writing, but it wasn't really anymore than what I had done during the Spring of Transition where I wrote 4 articles with 3890&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/06/20/spring-of-transition/"&gt;Spring of Transition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/05/24/using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path/"&gt;Using justpath to go on a pyrrhic adventure to clean up my PATH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/03/29/trying-out-pyenv-again/"&gt;Trying out pyenv ... again&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2024/03/19/winter-of-learning/"&gt;Winter of Learning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, the ability to transition from one idea to another is something that &lt;a href="https://www.youtube.com/watch?v=NVGuFdX5guE"&gt;CGP Grey talks about in his themes&lt;/a&gt; and so I don't feel too badly about it ... especially because the meditation and swimming have really been something I'm very proud of.&lt;/p&gt;
&lt;p&gt;My meditation practice includes a 20 minute daily session first thing in the morning to help clear my mind for the day.&lt;/p&gt;
&lt;p&gt;My swimming routine consists of swimming 3 days a week. When I started I could barely do a 20 minute swim. Just before DjangoCon US I swam 1800 yards in 54 minutes and for the most part didn't stop for a break. If you would have told me that in early July when I started I would not have believed you.&lt;/p&gt;
&lt;p&gt;In a weird way the meditation and swimming kind of helped with the writing because it allowed me to stop thinking, which then allowed room for deeper thinking about my writing.&lt;/p&gt;
&lt;p&gt;The stress of work has alleviated a bit and so I'm hoping that after DjangoCon US I'll be able to dedicate about 15 minutes a day to writing to start, and then ramp up to 30 minutes (similar to what I did with the swimming) and continue to swim and meditate.&lt;/p&gt;
&lt;p&gt;One thing that I've found very helpful is to just add a little bit of a good habit and remove a bit of a bad habit. Sooner or later the bad habit is gone and replaced with the new good habit.&lt;/p&gt;
&lt;p&gt;In the Autumn of mindfulness, which I will still try to do, I'll focus on eating right (I kind of eat like a 7 year old whose parents left the pantry stocked with a ton of junk food and then left for the weekend) so I'm going to work to get that under control.&lt;/p&gt;
&lt;p&gt;All in all, it's been a successful summer of writing, even if it wasn't what I initially envisioned. But that's OK, and part of life.&lt;/p&gt;
&lt;p&gt;As Mark Twain said, "Life is what happens when you're busy making other plans."&lt;sup id="sf-summer-of-writing-2-back"&gt;&lt;a href="#sf-summer-of-writing-2" class="simple-footnote" title="No he didn't. It was actually Allen Saunders in Reader's Digest in 1957, I just like to attribute everything to either Mark Twain or Abraham Lincoln"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-summer-of-writing-1"&gt;How much more? I don't really know ... just more &lt;a href="#sf-summer-of-writing-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-summer-of-writing-2"&gt;No he didn't. It was actually Allen Saunders in Reader's Digest in 1957, I just like to attribute everything to either Mark Twain or Abraham Lincoln &lt;a href="#sf-summer-of-writing-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="themes"></category></entry><entry><title>Mentors</title><link href="https://ryancheley.com/2024/09/16/mentors/" rel="alternate"></link><published>2024-09-16T00:00:00-07:00</published><updated>2024-09-16T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-09-16:/2024/09/16/mentors/</id><summary type="html">&lt;p&gt;Having just finished up my second round of Djangonaut.Space (which I wrote about &lt;a href="https://www.ryancheley.com/2024/08/29/reflections-on-djangonaut-space-session-2/"&gt;here&lt;/a&gt;) I wanted to write a bit about mentors ... how to find one, how to work with one, and how to be one.&lt;/p&gt;
&lt;h2&gt;Finding a Mentor&lt;/h2&gt;
&lt;p&gt;One of the best ways to find a mentor is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Having just finished up my second round of Djangonaut.Space (which I wrote about &lt;a href="https://www.ryancheley.com/2024/08/29/reflections-on-djangonaut-space-session-2/"&gt;here&lt;/a&gt;) I wanted to write a bit about mentors ... how to find one, how to work with one, and how to be one.&lt;/p&gt;
&lt;h2&gt;Finding a Mentor&lt;/h2&gt;
&lt;p&gt;One of the best ways to find a mentor is through a program like &lt;a href="https://djangonaut.space/"&gt;Djangonaut Space&lt;/a&gt;. You're put into a cohort of other Django / Python programmers with a Captain and a Navigator. A program like this offers up ready made mentors in the form of the Captains and Navigators. Even your fellow participants can act as mentors.&lt;/p&gt;
&lt;p&gt;The thing about a mentor, and finding one, is that what you're looking for isn't ONE mentor ... you're looking for a mentor in a specific aspect of life, whether personal or professional. In Djangonaut.Space you'll get a couple of mentors in Python / Django, but you may also find that you get a mentor who helps with thinking about / dealing with / finding developer jobs.&lt;/p&gt;
&lt;h2&gt;Working with a Mentor&lt;/h2&gt;
&lt;p&gt;Working with a mentor isn't just showing up and hoping that all of their knowledge in the specific aspect of life you're looking to be mentored on will suddenly flow from them to you like a fountain. You need to do a bit of homework too!&lt;/p&gt;
&lt;p&gt;Mentors can provide lots of guidance, but like any guide, you kind of need to know where you're going ... even if it's just a vague direction. Having a goal of&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want to be a programmer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;is a bit vague and difficult to help on. A mentor can provide some guidance for that, like&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Try Python&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;but a goal like,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want to learn programming to help automate some of these things&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;will definitely lead to more focused advice. Now the mentor can say,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That's great! Check out this book, these blogs, and follow this YouTuber ... also, here are 10 people you might find interesting on Mastodon (or your preferred Social Media platform of choice)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When working with a mentor they might provide open ended advice or guidance and expect that you'll have done something with it. Going back to the previous example, if a mentor offers the advice of Books, Blogs, YouTubers, etc at your next interaction they might ask, "So did you have a chance to check out any of those things".&lt;/p&gt;
&lt;p&gt;If your answer is no, that's not the end of the world, but it might signal to the mentor that you're not ready for the mentor/mentee relationship. If your answer is a bit more defined, like "No, work and family have really been crazy, but I've set aside 2 hours this weekend to really check them out" will help the mentor know that you're going to actively try and work on the suggestions made.&lt;/p&gt;
&lt;p&gt;Something to keep in mind is that this is a relationship with the mentor. They will try and provide helpful tips and guidance to you, and in return they expect that you'll be acting on those tips or guidance. If you're not willing or able to do that ... that's OK, but maybe this isn't the best time for your mentor relationship to start&lt;/p&gt;
&lt;h2&gt;How to be a mentor&lt;/h2&gt;
&lt;p&gt;Going back to my comment above, being a mentor isn't about being the ONLY mentor for a person, but a mentor for that person for a specific thing (or set of things) to help them grow. And that's really the point of mentoring. You want to help someone with their growth so that they can get better at a thing. This will have the strange effect of making you better at that thing as well.&lt;/p&gt;
&lt;p&gt;It's easy enough to wave your hands when you're thinking about why something works the way it does, but if you're mentoring someone and they ask you a question you don't know, you are going to do yourself a great service  by helping to explain and get them to understand the concept as well.&lt;/p&gt;
&lt;p&gt;For example, something that really breaks my brain is mocking. It's just never really stuck with me and every time I need to mock something I'm basically learning it over again. If I had a mentee and they asked about mocking I'd probably get a deer-in-the-headlights sort of look and then say,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You know, it's something I struggle with, but let me  write down some thoughts and my understanding on it and talk about it next time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And, here's the key, next time you meet with them talk through what you learned (again) and show them how you learned it. What docs did you reference? What applications of mocking did you try? How did you try and figure it out?&lt;/p&gt;
&lt;p&gt;I think so much of problem solving is learning how to learn. Honestly, if you can be presented with a problem and are able to come up with a solution without much thought then you may not understand the problem as well as you think. You might just be applying a previous solution to the current problem .. essentially trying to make a square peg try to fit in a round hole.&lt;/p&gt;
&lt;p&gt;But with mentoring you can help people learn how you learned and to guide them on their journey to discovering things.&lt;/p&gt;
&lt;p&gt;Something &lt;strong&gt;super&lt;/strong&gt; important to remember is that it's &lt;strong&gt;their&lt;/strong&gt; journey, not yours.&lt;/p&gt;
&lt;p&gt;One thing I do, probably too much, is tell stories to try and get people to understand and remember how things work. I find that stories really work for my brain and help me to retain details that are important, or help to remind me of the ways in which problems were solved.&lt;/p&gt;
&lt;p&gt;Honestly, every time someone comes to me with a new problem that I've never seen before my imposter syndrome kicks in like nobody's business! I beat myself up for how &lt;strong&gt;stupid&lt;/strong&gt; I am that I can't solve this problem that I've never seen before.&lt;/p&gt;
&lt;p&gt;But slowly, as I work through the problem, I start to see connections to other problems that I've solved. Not the same problem, but similar problems. This helps to get me to a solution ... but short walks help too ... and a good night's sleep.&lt;/p&gt;
&lt;p&gt;And this is a prime opportunity for you to take what you've learned, how you've learned it and help a mentee with finding an approach that helps them in similar situations.&lt;/p&gt;
&lt;p&gt;As a mentor, you don't need to be a WORLD EXPERT, you just need to be an expert on that one thing in comparison to the mentee. I once heard that an expert is just the person in the room who knows more about a topic than anyone else in that room. You don't need to be a Django Expert at DjangoCon to be a Django Expert at work when trying to introduce Django to developers that haven't seen it before.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Finding opportunities to be mentored can be hard, but a potential good place to start are programs like Djangonaut.Space and similar programs. Other places can be contributing to OSS projects&lt;sup id="sf-mentors-1-back"&gt;&lt;a href="#sf-mentors-1" class="simple-footnote" title="There are some caveats here, like an open and welcoming community"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Being a mentor doesn't mean you need to be a world expert, you just need to help one person find resources to help move them along in their journey. If you can do that, then I'd call you a pretty successful mentor!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-mentors-1"&gt;There are some caveats here, like an open and welcoming community &lt;a href="#sf-mentors-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="advice"></category></entry><entry><title>Reflections on Djangonaut Space Session 2</title><link href="https://ryancheley.com/2024/08/29/reflections-on-djangonaut-space-session-2/" rel="alternate"></link><published>2024-08-29T00:00:00-07:00</published><updated>2024-08-29T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-08-29:/2024/08/29/reflections-on-djangonaut-space-session-2/</id><summary type="html">&lt;p&gt;A few weeks ago I completed my second session as a &lt;a href="https://djangonaut.space/"&gt;Djangonaut.Space&lt;/a&gt; &lt;a href="https://github.com/djangonaut-space/program/blob/main/navigators.md"&gt;Navigator&lt;/a&gt;. The Djangonaut.Space program is an opportunity for people to be introduced to contributing to Django and Django adjacent projects.&lt;/p&gt;
&lt;p&gt;In this most recent session I was a Navigator for Team Mars with a fantastic Captain …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A few weeks ago I completed my second session as a &lt;a href="https://djangonaut.space/"&gt;Djangonaut.Space&lt;/a&gt; &lt;a href="https://github.com/djangonaut-space/program/blob/main/navigators.md"&gt;Navigator&lt;/a&gt;. The Djangonaut.Space program is an opportunity for people to be introduced to contributing to Django and Django adjacent projects.&lt;/p&gt;
&lt;p&gt;In this most recent session I was a Navigator for Team Mars with a fantastic Captain &lt;a href="https://www.linkedin.com/in/emmanuel-katchy"&gt;Tobe&lt;/a&gt;. Our Djangonauts were &lt;a href="https://softwarecrafts.uk/"&gt;Andy&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/maryam-yusuf/"&gt;Maryam&lt;/a&gt;, and &lt;a href="https://rosanarufer.blogspot.com/"&gt;Rosana&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Among the 3 of them they took on 7 tickets, pushed 7 PRs and closed 5 tickets.&lt;/p&gt;
&lt;p&gt;As part of the program we would meet weekly to talk about any blockers and try and work through them. These meetings also provided a platform to encourage one another.&lt;/p&gt;
&lt;p&gt;One week we spoke about being a professional software developer working with Django which was a great conversation.&lt;/p&gt;
&lt;p&gt;I really like this program for what it offers both the Djangonauts, and the mentors. I learned so much as part of this program.&lt;/p&gt;
&lt;p&gt;As we were coordinating our first meeting I realized that the rest of my team were in time zones that were 7 - 8 hours ahead of mine! I was a bit worried initially that we'd have a hard time finding a common time to meet, but we settled on Wednesdays at noon and this turned out to be pretty perfect for all of us.&lt;/p&gt;
&lt;p&gt;Each of our team meetings was similar to a &lt;a href="https://www.agile-academy.com/en/scrum-master/daily-standup/"&gt;standup&lt;/a&gt; where we'd talk about what work had been done the previous week, and any struggles that we were having. The djangonauts on team Mars were absolute Rock Stars. They picked up some pretty gnarly &lt;sup id="sf-reflections-on-djangonaut-space-session-2-1-back"&gt;&lt;a href="#sf-reflections-on-djangonaut-space-session-2-1" class="simple-footnote" title="Tickets 13376, 35464 and 12203"&gt;1&lt;/a&gt;&lt;/sup&gt; issues and worked them to completion each time.&lt;/p&gt;
&lt;p&gt;Working on a project like Django can be daunting and scary and time consuming. However, the amount that you can learn from working on a large project and code base like this is immeasurable.&lt;/p&gt;
&lt;p&gt;Working to form a consensus on an issue or idea, whether it's code or documentation, can be challenging! But as Maryam said in &lt;a href="https://maryam.hashnode.dev/contributing-to-django-with-djangonaut-space"&gt;her blog post&lt;/a&gt; about her experience with picking up a documentation ticket&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To start safely, I picked a documentation change ticket just to get myself familiar with the process. One of my tickets involved updating some wordings in the documentation to make it easier for people to differentiate when a pull request needed a Trac ticket or not. Initially, I thought this would be a simple wording change. However, I soon realised that making changes to Django documentation itself requires a lot of thought and consideration.&lt;/p&gt;
&lt;p&gt;This experience reminded me of my early days as a Django user. I loved Django for its documentation - detailed, thoughtful, well-organised, and easy to follow. Now, working on documentation changes as a contributor has shown me how Django achieves such clarity. Significant thought and effort go into making it clear and readable, minimising confusion and maximising understanding for readers.&lt;/p&gt;
&lt;p&gt;If you don't know this going in then you can be disappointed or disillusioned with how long something might take to be accepted, or whatever, but a program like Djangonaut Space does, I think, help to ease newcomers into contributing and setting realistic expectations and, in general, enjoying the process.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One thing I tried to really emphasize with my team was that it won't be easy, and it will take some time, but that the effort will pay off with a ticket that has been closed ... and in the worst case you've helped to move it forward.&lt;/p&gt;
&lt;p&gt;Another point I tried to keep front and center was the idea that this is a volunteer role and that if you're not having fun it's OK to take a step back. I think we need to hear that more and more, especially given the stress that many developers can be under for their $dayJobs.&lt;/p&gt;
&lt;p&gt;I hope that this advice helped them in navigating the tickets that they worked. I also hope it helped to put into perspective what they were doing from a time commitment perspective.&lt;/p&gt;
&lt;p&gt;One thing that I really love about the Django community in general, and the Djangonaut.Space community in particular, is how welcoming they are. The community strives to welcome you to be part of it.&lt;/p&gt;
&lt;p&gt;BUT even with the welcoming nature, it can still be very hard to pick that first ticket, submit that first PR, and receive that first bit of feedback.&lt;/p&gt;
&lt;p&gt;A program like Djangonaut.Space really helps to get people more comfortable with the process of picking and working on a ticket. It also helps to develop long term contributors to the project ... which is amazing.&lt;/p&gt;
&lt;p&gt;I'm looking forward to the next time I'll be able to participate and would encourage anyone to get involved, either as a participant, or as a mentor.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-reflections-on-djangonaut-space-session-2-1"&gt;Tickets &lt;a href="https://code.djangoproject.com/ticket/13376"&gt;13376&lt;/a&gt;, &lt;a href="https://code.djangoproject.com/ticket/35464"&gt;35464&lt;/a&gt; and &lt;a href="https://code.djangoproject.com/ticket/12203"&gt;12203&lt;/a&gt; &lt;a href="#sf-reflections-on-djangonaut-space-session-2-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="djangonauts"></category></entry><entry><title>How to ask why without sounding like a jerk</title><link href="https://ryancheley.com/2024/08/22/how-to-ask-why-without-sounding-like-a-jerk/" rel="alternate"></link><published>2024-08-22T00:00:00-07:00</published><updated>2024-08-22T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-08-22:/2024/08/22/how-to-ask-why-without-sounding-like-a-jerk/</id><summary type="html">&lt;p&gt;As technical folks working with non-technical folks sometimes the asks that come through are unclear. In order to get clarity on these we want to ask questions to get clarification on the ask, but it can be challenging to not sound like a jerk when we ask. This can happen …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As technical folks working with non-technical folks sometimes the asks that come through are unclear. In order to get clarity on these we want to ask questions to get clarification on the ask, but it can be challenging to not sound like a jerk when we ask. This can happen even IF we do our best to come across in a positive way.&lt;/p&gt;
&lt;p&gt;When trying to ask for more details on a project or request I find it's usually best to get to the source of the issue. I like to ask, "What problem are we trying to solve here?" or something similar.&lt;/p&gt;
&lt;p&gt;This helps to put you and the requester on 'the same team' trying to 'solve the problem' and not in a potentially negative 'why are you asking me this stupid question' sort of light.&lt;/p&gt;
&lt;p&gt;I can't say that I have 'one weird trick' that will always make this not a problem, but recently at my $dayJob I had an experience that might be helpful in seeing how to navigate this particular process.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;I received an email that went something like this&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Please see below. It seems that delivery of paper reports via courrier could be automated by sending them to a portal. What are your thoughts?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My initial thought was, "Yes, if we could automate these reports and send them electronically to a portal that would be more efficient."&lt;/p&gt;
&lt;p&gt;However, there are some deeper questions here that need to be asked ... like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why are we sending these reports in the first place?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Just asking this question though puts us into a potential state of conflict, i.e. it's similar to sounding like you're asking, "why would you do this stupid thing". In order to avoid this I reframed the question into 3 deeper questions that tried to frame 'the problem' and put me and the requester 'on the same team' to 'solve the problem'&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What are the reports?&lt;/li&gt;
&lt;li&gt;What are the recipients of the reports supposed to do with them?&lt;/li&gt;
&lt;li&gt;Do the recipients of the reports find them helpful, or do they just put them in the shred bin?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My first response to the sender was&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ideally any reports that are being delivered on printed paper by courrier would be better served to be delivered via some electronic means. Can you tell me, what are these reports and who are the intended recpients?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wanted to explicitly ask who the intended recipients were (I work in Healthcare and these reports are 'for the doctors' but they might actually be getting delivered to an office manager, a front desk person, or anyone other than the doctor).&lt;/p&gt;
&lt;p&gt;The sender responded back&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;They are reports that show a key metric for outstanding work left to do for a specific population of their membership. Each doctor (or their office) are free to do, or not do, anything with the information in these reports.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Next I asked if the recipients had been surveyed on the usefulness of the reports and that's when the sender indicated:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Actually, no. It's something that we need to do so that we can potentially consilidate reports and/or eliminate unhelpful reports.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;At the end we decided that before anywork was done to 'automate' the delivery of these reports, that we really needed to address the contents of the reports and determine which parts of them were helpful, and what parts weren't. Once we have a single report, or potentially a suite of reports, the automation and delivery work could actually start.&lt;/p&gt;
&lt;p&gt;By working through and trying to determine the actual problem that needed to be solved by asking questions to help both me and the requester better understand what the real ask was, we saved a ton of development time and have a better path forward for making the information we have more relevant and actionable by the doctors' offices.&lt;/p&gt;
&lt;p&gt;Will this work in every situation? Maybe not, but I believe it's a good starting point when trying to solve 'real world' problems in a work setting.&lt;/p&gt;
&lt;p&gt;Tech folks have a (sometimes deserved) bad wrap, but we can shed this negative impression by showing the people that request solutions from us that we're both working towards the same goal of solving the problem.&lt;/p&gt;</content><category term="musings"></category><category term="solutions"></category></entry><entry><title>SSH Keys</title><link href="https://ryancheley.com/2024/07/13/ssh-keys/" rel="alternate"></link><published>2024-07-13T00:00:00-07:00</published><updated>2024-07-13T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-07-13:/2024/07/13/ssh-keys/</id><summary type="html">&lt;p&gt;If you want to access a server in a 'passwordless' way, the best approach I know is to use SSH Keys. This is great, but what does that mean and how do you set it up?&lt;/p&gt;
&lt;p&gt;I'm going to attempt to write out the steps for getting this done.&lt;/p&gt;
&lt;p&gt;Let's …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you want to access a server in a 'passwordless' way, the best approach I know is to use SSH Keys. This is great, but what does that mean and how do you set it up?&lt;/p&gt;
&lt;p&gt;I'm going to attempt to write out the steps for getting this done.&lt;/p&gt;
&lt;p&gt;Let's assume we have two servers, &lt;code&gt;web1&lt;/code&gt; and &lt;code&gt;web2&lt;/code&gt;. These two servers have 1 non-root user which I'll call &lt;code&gt;user1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So we have something like this&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user1@web1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user1@web2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suppose we want to allow user1 from web2 to access web1.&lt;/p&gt;
&lt;p&gt;At a high level, we need to allow SSH access to web1 for user1 on web2 we need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;user1&lt;/code&gt; on &lt;code&gt;web1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;user1&lt;/code&gt; on &lt;code&gt;web2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create SSH keys on &lt;code&gt;web2&lt;/code&gt; for &lt;code&gt;user1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add the public key for &lt;code&gt;user1&lt;/code&gt; from &lt;code&gt;web2&lt;/code&gt; to onto the &lt;code&gt;authorized_keys&lt;/code&gt; for for &lt;code&gt;user1&lt;/code&gt; on &lt;code&gt;web1&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OK, let's try this. I am using DigitalOcean and will be taking advantage of their CLI tool &lt;code&gt;doctl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To create a droplet, there are two required arguments.:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;image&lt;/li&gt;
&lt;li&gt;size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm also going to include a few other options&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tag&lt;/li&gt;
&lt;li&gt;region&lt;/li&gt;
&lt;li&gt;ssh-keys&lt;sup id="sf-ssh-keys-1-back"&gt;&lt;a href="#sf-ssh-keys-1" class="simple-footnote" title="I'm using these keys so that I can gain access to the server as root"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Below is the command to use to create a server called &lt;code&gt;web-t-001&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;doctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;droplet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;001&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;24&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="n"&gt;vcpu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="n"&gt;gb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;monitoring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sfo2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="k"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and to create a server called &lt;code&gt;web-t-002&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;doctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;droplet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;002&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;24&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="n"&gt;vcpu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="n"&gt;gb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;monitoring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sfo2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="k"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The values for the &lt;code&gt;ssh-keys&lt;/code&gt; above will get all of the ssh-keys I have stored at DigitalOcean and add them.&lt;/p&gt;
&lt;p&gt;The output looks something like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;--ssh-keys 1234, 4567, 6789, 1222&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now that we've created two droplets called &lt;code&gt;web-t-001&lt;/code&gt; and &lt;code&gt;web-t-002&lt;/code&gt; we can set up user1 on each of the servers.&lt;/p&gt;
&lt;p&gt;I'll SSH as root into each of the servers and create &lt;code&gt;user1&lt;/code&gt; on each (I can do this because of the ssh keys that were added as part of the droplet creation)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;adduser --disabled-password --gecos "User 1" user1 --home /home/user1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I then switch to &lt;code&gt;user1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;su user1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and run this command&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh-keygen -q -t rsa -b 2048 -f /home/user1/.ssh/id_rsa -N ''&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This will generate two files in &lt;code&gt;~/.ssh without any prompts&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id_rsa&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id_rsa.pub&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;id_rsa&lt;/code&gt; identifies the computer. This is the Private Key. It should NOT be shared with anyone!&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;id_rsa.pub&lt;/code&gt;. This is the Public Key. It CAN be shared.&lt;/p&gt;
&lt;p&gt;The contents of the &lt;code&gt;id_rsa.pub&lt;/code&gt; file will be used for the &lt;code&gt;authorized_keys&lt;/code&gt; file on the computer this user will SSH into.&lt;/p&gt;
&lt;p&gt;OK, what does this actually look like?&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;web-t-002&lt;/code&gt;, I get the content of &lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt; for &lt;code&gt;user1&lt;/code&gt; and copy it to my clipboard.&lt;/p&gt;
&lt;p&gt;Then from &lt;code&gt;web-t-001&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt; I paste that into the the &lt;code&gt;authorized_keys&lt;/code&gt; in &lt;code&gt;~/.ssh&lt;/code&gt;. If the file isn't already there, it needs to be created.&lt;/p&gt;
&lt;p&gt;This tells &lt;code&gt;web-t-001&lt;/code&gt; that a computer with the private key that matches the public key is allowed to access as &lt;code&gt;user1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The implementation of this is to be on &lt;code&gt;web-t-002&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt; and then run the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="nv"&gt;@web&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first time this is done, a prompt will come up that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;authenticity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;established&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;ED25519&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;:....&lt;/span&gt;
&lt;span class="n"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;Are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connecting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;no&lt;/span&gt;&lt;span class="o"&gt;/[&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="vm"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This ensure that you know which computer you're connecting to and you want to continue. This helps to prevent potential man-in-the-middle attacks. When you type &lt;code&gt;yes&lt;/code&gt; this creates a file called &lt;code&gt;known_hosts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;OK, so where are we at? The table below shows the files, their content, and their servers&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;id_rsa&lt;/th&gt;
&lt;th&gt;id_rsa.pub&lt;/th&gt;
&lt;th&gt;authorized_keys&lt;/th&gt;
&lt;th&gt;known_hosts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;web-t-001&lt;/td&gt;
&lt;td&gt;private key&lt;/td&gt;
&lt;td&gt;ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDFwHs8VKWWSH737fVz4cs+5Eq8OcRJRf2ti0ytaChM1ySh2+olcKokHao3fl5G+ZZv4pQeKfCh8ClFP86g7rZN1evu2EFVlmBo1Ked4IwF4UBY2+rnfZmvxeHd+smtyZgfVZI/6ySfe1D+inAqv7otsMsNRRuE4aG0DNEJ39qwFxukGNcDXk9RNVvmwbCc5zT/HN0yMJ6Y7KtfPZgjl5v854VodZkfxsLpah7Bn64zAQr/xDh2KcWbtDrsvTdjNMPY7oW20VoqDs98mA6xAw9RNMI+xotNmivdWdv3BEYj9JyH61euTBQ27HC4LsOPuCOFKBqOwGXiJhpzvJZbNCcvQEztem3kqQFAPLg+4wBInyxnY2i31QX7+2IJs0a4pYTWRSRcrvwBAvi2GlXGltrZ7V6KOLzwBrXLD7XiO3C5kO5fcpanKlm/RdVAxUTjUq159H+v9om8HAgX/pIpYBpPnRrG7setNQVzDNQsxfR/YC0h+f9LWnnaBV6+51IjbaqAPSSf6KYv0AKO5XNlJsSTXNRBZaRvrfr0qllgXU82f9y8Eb0sgjL71wD9Fv24fV0toFW8PH3yOeePC6d7kNqZkFdSBksChzqagZwPudYnVhMmhMYV7k1v831H8WHdGPVRe9Z3BDnSCzf8o8fRS3mSEAJBiT30bXlGWUNopIpsgw== user1@ahc-web-t-001&lt;/td&gt;
&lt;td&gt;ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCbx+wTVEcdy2Uu2iB+u6+R8Q0yH9ws92GM6K/XXmAXoUuXylkdJzw9vUeuaZTmGxwGRdp+lLh+vVDmiuzrUPjbkFA7Y1SxfR5lgJu7PviDDZzsFeUo5fqSp6FOC5x75jOjqy6fc68GzOnoxk4WR6EWKWRd+xqdgCTGWiuhfUEl1lw7YN8MUhd1Hi0Ef55ZpH133jCzffWbkLFFInyIwuzG6jaPsobNPRshvg9kUoFwo5WqCx/s8Zk4iVl86yCwoV+pXjiubLylSKF7hb7uDE4Ll8gADOtuXUqmc470yvzSxxI4yaZOFz4Ajo1qZHgscSOxWgb+ZVIOKhGK5ftHPaZ4CCxXuhW5J8L3Aqs0WQeRu9Goof83V/ruZhzgg1vnhmC2511QSS2dL6U7n2JNLtNnXNjeSQ0BGVlY1FuZRczmAxN9nJETmRCdUfiTwKdPS4LdfAwrnckPHKtk1QoFKietLwfbmipU+pGvt6qKpKeRfZ/XGbG+ZiQ7oPiqcYU/eh54IAUxxo9CvVHtn742A4ABqK5+0MJP5VuY3fcDU8dIvA0r4LpxRpG/KSB4yZMUhjf+KR7QUpN3mJIDOKTDAxpGOqpNoD2gTYGpyT13AdrRROpOjOJZJqDiVi6m6r/U+sIgqymsxDqBur5+n4VxvvXbdNd+6vz7AI12WA8I8+0xZw== user1@ahc-web-t-002&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web-t-002&lt;/td&gt;
&lt;td&gt;private key&lt;/td&gt;
&lt;td&gt;ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCbx+wTVEcdy2Uu2iB+u6+R8Q0yH9ws92GM6K/XXmAXoUuXylkdJzw9vUeuaZTmGxwGRdp+lLh+vVDmiuzrUPjbkFA7Y1SxfR5lgJu7PviDDZzsFeUo5fqSp6FOC5x75jOjqy6fc68GzOnoxk4WR6EWKWRd+xqdgCTGWiuhfUEl1lw7YN8MUhd1Hi0Ef55ZpH133jCzffWbkLFFInyIwuzG6jaPsobNPRshvg9kUoFwo5WqCx/s8Zk4iVl86yCwoV+pXjiubLylSKF7hb7uDE4Ll8gADOtuXUqmc470yvzSxxI4yaZOFz4Ajo1qZHgscSOxWgb+ZVIOKhGK5ftHPaZ4CCxXuhW5J8L3Aqs0WQeRu9Goof83V/ruZhzgg1vnhmC2511QSS2dL6U7n2JNLtNnXNjeSQ0BGVlY1FuZRczmAxN9nJETmRCdUfiTwKdPS4LdfAwrnckPHKtk1QoFKietLwfbmipU+pGvt6qKpKeRfZ/XGbG+ZiQ7oPiqcYU/eh54IAUxxo9CvVHtn742A4ABqK5+0MJP5VuY3fcDU8dIvA0r4LpxRpG/KSB4yZMUhjf+KR7QUpN3mJIDOKTDAxpGOqpNoD2gTYGpyT13AdrRROpOjOJZJqDiVi6m6r/U+sIgqymsxDqBur5+n4VxvvXbdNd+6vz7AI12WA8I8+0xZw== user1@ahc-web-t-002&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;|1|V6uYGlSiYXpzFAly9RQHybzl07o=|VUkDfRcKGyUgLdJn+iw6RJE+r68= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILpbPHA1jL0MHzBI8qb2X0mHDx3UlrKCdbz1IspvaJW9  |1|dshOpqJI2zQxEpj1pleDmtkijIY=|ZYV8bCeLNDdyE7STDPaO2TzYUEQ= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDnGgQUmsCG23b6iYxRHq5MU9xd8Q/p8j3EyZn9hvs4IsBoCgeNXjyXK28x7Mt7tmfjrF/4jLcq4o2TTAwF6eVQZ4KXoBa73dYqYDmYTVKTwzZL9CsJTWHTsSnU8V/J3Tml+hIFrjZzWP34+lL9xyOVin5R0PT/OCG49ecb5tt2FxTZeyWI47B/bCDGXV9g1tjZ8+mnbLXpIdQ9+6GllRZrEGvXWm6z/U3YHO84dcG0IZJ7QsEaAiLSBC/t83So4MDQgdttm+aHZXds4jej5E3QwUex8JkVVn0X7Nr4yKMDkSk7ABD6AFhpa4ESXysqI33CUaSBROAuu4lmfOkLmyRZK2vQ6soiOW8iBgCEl/q8MSOEpZeAi3faYbUnOpLzLDBcCoAuSDoexrTixxlhJmRDeS3PlcXmzvkJl7RRKUYZZcPQOd2w9ipCIAD1PevNlnmmZcfkRe0RRvAyF1mqcO/x5Ovtq9QLbycFHYfh/3LcPuDOWBtT+mVd5FeNUMsZ6+8=  |1|HJd+aDFM66x8jJT1zUZV59ceL10=|PfHQu/Yg35QPBKk7FvNO/46b76o= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBP+XwUozGye03WJ6zC7yoJQaYF8HiUQKmZwnQO0wSxMm9x9nBdPEx1bmyZHHUbMnwQnoeAMmd6hgK6H8hbxzEas=&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;OK, so now we have set it up so that &lt;code&gt;user1&lt;/code&gt; on &lt;code&gt;web-t-002&lt;/code&gt; can access &lt;code&gt;web-t-001&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt;. A reasonable question to ask at this point might be, can I go the other way without any extra steps?&lt;/p&gt;
&lt;p&gt;Let's try it and see!&lt;/p&gt;
&lt;p&gt;From &lt;code&gt;web-t-001&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt; lets run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="nv"&gt;@web&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And see what happens&lt;/p&gt;
&lt;p&gt;We get the same warning message we got before, and if we enter &lt;code&gt;yes&lt;/code&gt; we then see&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="nv"&gt;@yyy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yyy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yyy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nl"&gt;yyy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Permission&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;denied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publickey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What's going on?&lt;/p&gt;
&lt;p&gt;We haven't added the public key for &lt;code&gt;user1&lt;/code&gt; from &lt;code&gt;web-t-002&lt;/code&gt; to the &lt;code&gt;authorized_keys&lt;/code&gt; file on &lt;code&gt;web-t-001&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let's do that now.&lt;/p&gt;
&lt;p&gt;Similar to before we'll get the content of the id_rsa.pub from &lt;code&gt;web-t-001&lt;/code&gt; for &lt;code&gt;user1&lt;/code&gt; and copy it to the &lt;code&gt;authorized_keys&lt;/code&gt; file on &lt;code&gt;web-t-002&lt;/code&gt; for &lt;code&gt;user1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When we do this we are now able to connect to &lt;code&gt;web-t-002&lt;/code&gt; from &lt;code&gt;web-t-001&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;OK, SSH has 4 main files involved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id_rsa.pub&lt;/code&gt; (public key): The SSH key pair is used to authenticate the identity of a user or process that wants to access a remote system using the SSH protocol. The public key is used by both the user and the remote server to encrypt messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id_rsa&lt;/code&gt; (private key): The private key is secret, known only to the user, and should be encrypted and stored safely&lt;/li&gt;
&lt;li&gt;&lt;code&gt;known_hosts&lt;/code&gt;: stores the public keys and fingerprints of the hosts accessed by a user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authorized_keys&lt;/code&gt;: file containing all of the authorized public keys that have been generated. This is what tells the server that it’s Ok to use the key to allow a connection&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Using with GHA&lt;/h2&gt;
&lt;h3&gt;ssh-action from AppleBoy&lt;/h3&gt;
&lt;p&gt;A general way to access your server with GHA (say for CICD) is to use the &lt;a href="https://github.com/appleboy/ssh-action"&gt;GitHub action from appleboy&lt;/a&gt; called &lt;code&gt;ssh-action&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There are 3 key components needed to get this to work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;host&lt;/li&gt;
&lt;li&gt;username&lt;/li&gt;
&lt;li&gt;key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these can/should be put into repository secrets. Setting those up is outside the scope of this article. For details on how to set repository secrets, see &lt;a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions"&gt;this&lt;/a&gt; article.&lt;/p&gt;
&lt;p&gt;Using the servers from above we could set up the following secrets&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH_HOST: web-t-001 IP Address&lt;/li&gt;
&lt;li&gt;SSH_KEY: the content from /home/user1/.ssh/id_rsa ( from web-t-002 )&lt;/li&gt;
&lt;li&gt;SSH_USERNAME: user1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And then set up an GitHub Action like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Test Workflow&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-22.04&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;deploy code&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;appleboy/ssh-action@v0.1.10&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.SSH_HOST }}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;22&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.SSH_KEY }}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.SSH_USERNAME }}&lt;/span&gt;

&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;echo "This is a test" &amp;gt;  ~/test.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using this set up we've made the docker image that runs the GHA to be (basically) known as &lt;code&gt;web-t-001&lt;/code&gt; and it has access as &lt;code&gt;user1&lt;/code&gt; in the same way we did in the terminal.&lt;/p&gt;
&lt;p&gt;When this action is run it will ssh into &lt;code&gt;web-t-001&lt;/code&gt; as &lt;code&gt;user1&lt;/code&gt; and create a file called &lt;code&gt;test.txt&lt;/code&gt; in the home directory. The content of that file will be "This is a test"&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-ssh-keys-1"&gt;I'm using these keys so that I can gain access to the server as root &lt;a href="#sf-ssh-keys-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="tutorial"></category><category term="ssh keys"></category></entry><entry><title>Spring of Transition</title><link href="https://ryancheley.com/2024/06/20/spring-of-transition/" rel="alternate"></link><published>2024-06-20T00:00:00-07:00</published><updated>2024-06-20T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-06-20:/2024/06/20/spring-of-transition/</id><summary type="html">&lt;p&gt;I've written before about the &lt;a href="https://youtu.be/NVGuFdX5guE?si=-9zFaB0xjmxOEh26"&gt;Theme's that CGP Grey&lt;/a&gt; has discussed and I think they're great! I've just recently completed my 'Spring of Transition'.&lt;/p&gt;
&lt;p&gt;So what is the Spring of Transition? For me it meant focusing on that last bit of time that my daughter will be living with me …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've written before about the &lt;a href="https://youtu.be/NVGuFdX5guE?si=-9zFaB0xjmxOEh26"&gt;Theme's that CGP Grey&lt;/a&gt; has discussed and I think they're great! I've just recently completed my 'Spring of Transition'.&lt;/p&gt;
&lt;p&gt;So what is the Spring of Transition? For me it meant focusing on that last bit of time that my daughter will be living with me and my wife full time.&lt;/p&gt;
&lt;p&gt;She just graduated from High School and is getting ready to go off to college in the Fall. I've taken the last quarter to really try and focus on spending quality time with her, and enjoy the last bits of her living here with me and my wife.&lt;/p&gt;
&lt;p&gt;One of the things I am eternally grateful for is that when my daughter was a baby/toddler I started a &lt;a href="https://wordpress.org/"&gt;WordPress&lt;/a&gt; blog to keep track of all of the adventures we got into. This was 2008 - 2010 and while Facebook was &lt;em&gt;kind&lt;/em&gt; of a thing, Instragram was &lt;strong&gt;NOT&lt;/strong&gt; a thing. I used this blog to post pictures with a fun caption of the context of the picture. I would also write a monthly letter to her and recap what fun adventures we had, or what changes I had noticed.&lt;/p&gt;
&lt;p&gt;I did this for a couple of years, but then life got in the way and the changes that she was going through were harder and harder to see, and capture, with a camera. This made it very hard to write about as well.&lt;/p&gt;
&lt;p&gt;I ended up taking down my site, but I kept a backup of the WordPress XML just in case&lt;sup id="sf-spring-of-transition-1-back"&gt;&lt;a href="#sf-spring-of-transition-1" class="simple-footnote" title="this is one of the ONLY times my digital hoarding has actually paid off"&gt;1&lt;/a&gt;&lt;/sup&gt; I would want to use it again.&lt;/p&gt;
&lt;p&gt;At one point I stumbled upon a journalling app called &lt;a href="https://dayoneapp.com/"&gt;Day One&lt;/a&gt; that I used to journal. I used it for a couple of years and then found a feature that allowed me to import my WordPress blog data.&lt;/p&gt;
&lt;p&gt;I played around with this a bit and finally made the plunge to import the data. It may be the best decision I've ever made with respect to tech.&lt;/p&gt;
&lt;p&gt;Over the last several years I focused on trying to journal every day. One of the grear features of Day One is 'On This Day'. After I journal I'll click on that tab and look back at what I've written "On This Day".&lt;/p&gt;
&lt;p&gt;The best entries are from the blog. Small reminders of the toddler my (now) adult daughter was.&lt;/p&gt;
&lt;p&gt;This has been especially great over the last 3 months as she has wrapped up High School and prepared for College. It's really allowed me to focus on the great times we had, and work to create some awesome new memories.&lt;/p&gt;
&lt;p&gt;We didn't do anything exotic, or visit any far off places during this 'Spring of Transition' ... there's already sooooo much to be done at the end of a High School career!&lt;/p&gt;
&lt;p&gt;But, I have tried to focus on the things that we do like to do.&lt;/p&gt;
&lt;p&gt;We have watched a bunch of Star Wars:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Star_Wars:_The_Bad_Batch"&gt;Bad Batch Season 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Acolyte_(TV_series)"&gt;Acolyte&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Star_Wars:_Tales#Tales_of_the_Empire_(2024)"&gt;Tales of the Empire&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We've also tried to watch the &lt;a href="https://en.wikipedia.org/wiki/Battlestar_Galactica_(2004_TV_series)"&gt;Battle Star Galactica TV&lt;/a&gt; show from the early 2000s. This didn't go well.&lt;/p&gt;
&lt;p&gt;Another thing that we've been doing is trying to cook meals together. My daughter is a vegetarian, and my wife and I are not, so this makes dinner (and other meals) challenging.&lt;/p&gt;
&lt;p&gt;To over come this she found several vegetarian dishes that she thought I would like and we've made them together for dinner. It's been a real treat to see what she thinks of some of these recipes, which are mostly Thai and Vietnamese meals which is not something she would typically eat (she's really big into the various combinations of cheese and starch, i.e. Grilled Cheese Sandwiches, Quesadillas, and Cheese Pizza 😊&lt;/p&gt;
&lt;p&gt;We've also been trying really hard to get take out from a Thai place that has many vegetarian options, but haven't been able to make it work just yet. This looks to be something that we'll &lt;strong&gt;hopefully&lt;/strong&gt; be able to do over the summer before she leaves for school.&lt;/p&gt;
&lt;p&gt;I think the hardest part about all of this has been knowing that each of the things that she's done will be the last. She had her last dance recital (I've been watching her dance for 15 years). She had her last Girl Scout meeting. She had her last High School class.&lt;/p&gt;
&lt;p&gt;Soon she'll have her last night sleeping here before she goes to school.&lt;/p&gt;
&lt;p&gt;Obviously I always knew this day would come, but I didn't really think it would get here so quickly. &lt;a href="https://www.goodreads.com/quotes/239043-the-days-are-long-but-the-years-are-short#:~:text=Quote%20by%20Gretchen%20Rubin%3A%20%E2%80%9CThe,but%20the%20years%20are%20short.%E2%80%9D"&gt;The days are long, but the years are short&lt;/a&gt;. I never &lt;strong&gt;really&lt;/strong&gt; understood what that meant until these last few months.&lt;/p&gt;
&lt;p&gt;There is now this full grown adult living in my home ... at least for the next few months. But just yesterday she was a silly toddler walking around the house claiming that the elves must have left her milk in the pantry!&lt;/p&gt;
&lt;p&gt;I know that her leaving for college isn't the last time I'll ever see her. I mean, she'll still have a room at our house, so she'll want to come back at some point, right? Right?&lt;/p&gt;
&lt;p&gt;And it's not like she's going to school on the other side of the country. It's just a short 2 hour drive away. But still ... it won't be the same.&lt;/p&gt;
&lt;p&gt;It's just all so different. My wife and I are planning to be empty nesters. Like, what does that even mean? For the last 19 years our daughter has been a part of, and influenced, the lives that we've lead.&lt;/p&gt;
&lt;p&gt;I'm also a little nervous about my 'little girl'&lt;sup id="sf-spring-of-transition-2-back"&gt;&lt;a href="#sf-spring-of-transition-2" class="simple-footnote" title="Fun fact: she's 5'7 with the strength of someone that's been dancing for 15 years so she's not actually little"&gt;2&lt;/a&gt;&lt;/sup&gt; going away into the big bad world. I know I shouldn't be though. She is the most thoughtful, capable, intelligent, caring, hard working person I've ever met in my life.&lt;/p&gt;
&lt;p&gt;I know she's going to do great in her next chapter.&lt;/p&gt;
&lt;p&gt;I just didn't realize that next chapter would come so soon.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-spring-of-transition-1"&gt;this is one of the ONLY times my digital hoarding has actually paid off &lt;a href="#sf-spring-of-transition-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-spring-of-transition-2"&gt;Fun fact: she's 5'7 with the strength of someone that's been dancing for 15 years so she's not &lt;em&gt;actually&lt;/em&gt; little &lt;a href="#sf-spring-of-transition-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="themes"></category></entry><entry><title>Using justpath to go on a pyrrhic adventure to clean up my PATH</title><link href="https://ryancheley.com/2024/05/24/using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path/" rel="alternate"></link><published>2024-05-24T00:00:00-07:00</published><updated>2024-05-24T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-05-24:/2024/05/24/using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path/</id><summary type="html">&lt;p&gt;A while ago I heard about a project called &lt;a href="https://github.com/epogrebnyak/justpath"&gt;justpath&lt;/a&gt; from &lt;a href="https://mastodon.social/@webology"&gt;Jeff Tripplet&lt;/a&gt; on &lt;a href="https://mastodon.social/@webology/112403455881574563"&gt;Mastodon&lt;/a&gt;. It seemed like a neat project to try and clean up my path and I figured, what the heck, let me give it a try.&lt;/p&gt;
&lt;p&gt;I installed it and when I ran it for the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A while ago I heard about a project called &lt;a href="https://github.com/epogrebnyak/justpath"&gt;justpath&lt;/a&gt; from &lt;a href="https://mastodon.social/@webology"&gt;Jeff Tripplet&lt;/a&gt; on &lt;a href="https://mastodon.social/@webology/112403455881574563"&gt;Mastodon&lt;/a&gt;. It seemed like a neat project to try and clean up my path and I figured, what the heck, let me give it a try.&lt;/p&gt;
&lt;p&gt;I installed it and when I ran it for the first time, the output looked like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;cargo&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Cellar&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;_1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;pyenv&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shims&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;homebrew&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Cellar&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pyenv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;virtualenv&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shims&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;homebrew&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;homebrew&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sbin&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Frameworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sbin&lt;/span&gt;
&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Frameworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.11&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Frameworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Cryptexes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Volumes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Preboot&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Cryptexes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sbin&lt;/span&gt;
&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sbin&lt;/span&gt;
&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;appleinternal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;appleinternal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Apple&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;TeX&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;texbin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;texlive&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;universal&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;darwin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;
&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Frameworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Frameworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;6.12&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Applications&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Postgres&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Contents&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Applications&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Postgres&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Contents&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Applications&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Xamarin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Workbooks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Contents&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SharedSupport&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's a lot to look at, but helpfully there are a few flags to get only the 'bad' items&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;justpath --invalid

justpath --duplicates
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running &lt;code&gt;justpath --invalid&lt;/code&gt; got this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;18&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;appleinternal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cryptexd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codex&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;appleinternal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="n"&gt;ols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;29&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Applications&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Xamarin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Workbooks&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;Cont&lt;/span&gt;&lt;span class="n"&gt;ents&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SharedSupport&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and running &lt;code&gt;justpath --duplicates&lt;/code&gt; got this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 8 /usr/local/bin (duplicates: 3)
12 /usr/local/bin (duplicates: 3)
30 /Users/ryan/.local/bin (duplicates: 2)
31 /usr/local/bin (duplicates: 3)
32 /Users/ryan/.local/bin (duplicates: 2)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great! Now I know what is invalid and what is duplicated. Surely &lt;code&gt;justpath&lt;/code&gt; will have a command to clean this up, right?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Anakin Padmeme Meme" src="https://ryancheley.com/images/justpath-anakin-padme-meme.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Turns out not so much, and for good &lt;a href="https://www.reddit.com/r/Python/comments/1aehs4i/comment/kk89bor/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button"&gt;reasons&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;justpath&lt;/code&gt; does not alter the path itself, it provides the corrected version that a user can apply further. A child process the one that Python is running in cannot alter the parent environment PATH. As for reading the PATH - &lt;code&gt;justpath&lt;/code&gt; relies on what is available from &lt;code&gt;os.environ['PATH']&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I posted on Mastodon to see how others may have approached this and Jeff &lt;a href="https://mastodon.social/@webology/112403455881574563"&gt;replied back&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I ran the tool and copied the output before starting from an empty PATH.&lt;/p&gt;
&lt;p&gt;Then I ran justpath and added them back one at a time.&lt;/p&gt;
&lt;p&gt;mise helped me cut at least half a dozen weird/duplicate path statements alone. I had quite a bit of tool overlap.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I poked around with trying to find where the invalid values were coming from and found &lt;a href="https://apple.stackexchange.com/a/458280"&gt;this&lt;/a&gt; answer&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cryptexes are used to update parts of macOS quickly, without requiring a full rebuild of the SSV (see &lt;a href="https://eclecticlight.co/2023/04/05/how-cryptexes-are-changing-macos-ventura/"&gt;this&lt;/a&gt; answer for details).
I don't know whether removing these paths will break the installation of such cryptexes. But if you want to take the risk, you can remove &lt;code&gt;/etc/paths.d/10-cryptex&lt;/code&gt; (or move it to a safe place in case you need it later on).
PS: Invalid entries in PATH don't really hurt, they primarily slow down (a very little bit) the lookup of new commands run from the shell the first time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This supported my assumption that the invalid PATH variables were likely due to macOS upgrades (I guessed that based on the existence of invalid PATH variables on other Macs in my house that didn't have a programmer / developer using them)&lt;/p&gt;
&lt;p&gt;So, with that I found 2 files that were in &lt;code&gt;/etc/paths.d&lt;/code&gt; and moved them to my desktop. Once that was done I restarted my terminal and got this output&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 1 /Users/ryan/.cargo/bin
 2 /usr/local/opt/ruby/bin (resolves to /usr/local/Cellar/ruby/3.2.2_1/bin)
 3 /Users/ryan/google-cloud-sdk/bin
 4 /Users/ryan/.pyenv/shims
 5 /opt/homebrew/Cellar/pyenv-virtualenv/1.2.3/shims
 6 /opt/homebrew/bin
 7 /opt/homebrew/sbin
 8 /Library/Frameworks/Python.framework/Versions/3.12/bin
 9 /usr/local/bin (duplicates: 3)
10 /usr/local/sbin
11 /Library/Frameworks/Python.framework/Versions/3.11/bin
12 /Library/Frameworks/Python.framework/Versions/3.10/bin
13 /usr/local/bin (duplicates: 3)
14 /System/Cryptexes/App/usr/bin (resolves to /System/Volumes/Preboot/Cryptexes/App/usr/bin)
15 /usr/bin
16 /bin
17 /usr/sbin
18 /sbin
19 /opt/X11/bin
20 /Library/Apple/usr/bin
21 /Library/TeX/texbin (resolves to /usr/local/texlive/2024basic/bin/universal-darwin)
22 /usr/local/share/dotnet
23 /Library/Frameworks/Mono.framework/Versions/Current/Commands (resolves to /Library/Frameworks/Mono.framework/Versions/6.12.0/Commands)
24 /Applications/Postgres.app/Contents/Versions/latest/bin (resolves to /Applications/Postgres.app/Contents/Versions/14/bin)
25 /Users/ryan/.local/bin (duplicates: 2)
26 /usr/local/bin (duplicates: 3)
27 /Users/ryan/.local/bin (duplicates: 2)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This just left the duplicates that were being added. There are a few spots where applications (or people!) will add items to PATH and there are MANY opinions on which one is The Right Way TM. I use &lt;a href="https://en.wikipedia.org/wiki/Z_shell"&gt;zshell&lt;/a&gt; and looked in each of these files (&lt;code&gt;~/.profile&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt;, &lt;code&gt;~/.zprofile&lt;/code&gt;) and found that only my &lt;code&gt;.zshrc&lt;/code&gt; file contained the addition to PATH for the duplicates I was seeing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/local/bin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/Users/ryan/.local/bin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that, I simply commented them out of my &lt;code&gt;.zshrc&lt;/code&gt;, restarted my terminal and now I am down to only two duplicates duplicate&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 8 /usr/local/bin (duplicates: 2)
12 /usr/local/bin (duplicates: 2)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I looked &lt;em&gt;everywhere&lt;/em&gt; trying to find where this duplicate was coming from. I tried a few variations of &lt;code&gt;find&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;H&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But the results were only the commented out lines from &lt;code&gt;.zshrc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I did find &lt;a href="https://github.com/Homebrew/brew/issues/14560"&gt;this issue&lt;/a&gt; on the Homebrew repo and thought I might be onto something, but it was for a different path so that doesn't seem to be &lt;strong&gt;the&lt;/strong&gt; culprit.&lt;/p&gt;
&lt;p&gt;I eventually gave up on trying to find the one true source of the duplicate entry (though I suspect that there is something adding it in the same way that Homebrew is can add a duplicate PATH variable) because I &lt;a href="https://stackoverflow.com/a/30792333"&gt;found&lt;/a&gt; this command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;typeset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I added it to my &lt;code&gt;.zshrc&lt;/code&gt;, restarted my terminal and voila, a clean PATH&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 1 /Users/ryan/.cargo/bin
 2 /Users/ryan/google-cloud-sdk/bin
 3 /Users/ryan/.pyenv/shims
 4 /opt/homebrew/Cellar/pyenv-virtualenv/1.2.3/shims
 5 /opt/homebrew/bin
 6 /opt/homebrew/sbin
 7 /Library/Frameworks/Python.framework/Versions/3.12/bin
 8 /usr/local/bin
 9 /usr/local/sbin
10 /Library/Frameworks/Python.framework/Versions/3.11/bin
11 /Library/Frameworks/Python.framework/Versions/3.10/bin
12 /System/Cryptexes/App/usr/bin (resolves to /System/Volumes/Preboot/Cryptexes/App/usr/bin)
13 /usr/bin
14 /bin
15 /usr/sbin
16 /sbin
17 /opt/X11/bin
18 /Library/Apple/usr/bin
19 /Library/TeX/texbin (resolves to /usr/local/texlive/2024basic/bin/universal-darwin)
20 /usr/local/share/dotnet
21 /Library/Frameworks/Mono.framework/Versions/Current/Commands (resolves to /Library/Frameworks/Mono.framework/Versions/6.12.0/Commands)
22 /Applications/Postgres.app/Contents/Versions/latest/bin (resolves to /Applications/Postgres.app/Contents/Versions/16/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I still want to try and figure out where that extra &lt;code&gt;/usr/local/bin&lt;/code&gt; is coming from, but for now, I have been able to clean up my PATH.&lt;/p&gt;
&lt;p&gt;So, the question is, does this really matter? In the &lt;a href="https://apple.stackexchange.com/a/458280"&gt;answer&lt;/a&gt; I found the last statement really caught my attention&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS: Invalid entries in PATH don't really hurt, they primarily slow down (a very little bit) the lookup of new commands run from the shell the first time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the answer is, probably not, BUT just knowing that I had duplicates and invalid entries in my PATH was like a splinter in my brain &lt;sup id="sf-using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path-1-back"&gt;&lt;a href="#sf-using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path-1" class="simple-footnote" title="Yes, that is a Matrix reference"&gt;1&lt;/a&gt;&lt;/sup&gt; that needed to be excised.&lt;/p&gt;
&lt;p&gt;What are your experiences with your PATH? Have you gone to these lengths to clean it up, figured, "Meh, it's not hurting anything, why bother?"&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path-1"&gt;Yes, that is a Matrix reference &lt;a href="#sf-using-justpath-to-go-on-a-pyrrhic-adventure-to-clean-up-my-path-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="just"></category><category term="path"></category></entry><entry><title>Trying out pyenv ... again</title><link href="https://ryancheley.com/2024/03/29/trying-out-pyenv-again/" rel="alternate"></link><published>2024-03-29T00:00:00-07:00</published><updated>2024-03-29T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-03-29:/2024/03/29/trying-out-pyenv-again/</id><summary type="html">&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; I first tried &lt;code&gt;pyenv&lt;/code&gt; probably sometime in late 2022. I saw some recent stuff about it on Mastadon and thought I'd give it another go.&lt;/p&gt;
&lt;p&gt;I read through the &lt;a href="https://github.com/pyenv/pyenv/#installation"&gt;installation instructions at the ReadMe&lt;/a&gt; at the repo and checked to see if it was already installed (spoiler alert …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; I first tried &lt;code&gt;pyenv&lt;/code&gt; probably sometime in late 2022. I saw some recent stuff about it on Mastadon and thought I'd give it another go.&lt;/p&gt;
&lt;p&gt;I read through the &lt;a href="https://github.com/pyenv/pyenv/#installation"&gt;installation instructions at the ReadMe&lt;/a&gt; at the repo and checked to see if it was already installed (spoiler alert it was!)&lt;/p&gt;
&lt;p&gt;I noticed that I was not on the current version (2.3.36 at the time of this writing) and decided that I needed to &lt;a href="https://docs.brew.sh/FAQ#how-do-i-update-my-local-packages"&gt;update it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With the update out of the way I tried to install a version of Python with it, starting at Python 3.10 (because why not?!)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pyenv install 3.10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But when I ran it I got an error like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;BUILD FAILED (OS X 12.3.1 using python-build 20180424)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which lead me &lt;a href="https://github.com/pyenv/pyenv/issues/2343"&gt;here&lt;/a&gt;. There were some comments people left about deleting directories (which always makes me a bit uneasy ... especially when they're in /Library/)&lt;/p&gt;
&lt;p&gt;Reading further down I did come across &lt;a href="https://github.com/pyenv/pyenv/issues/2343#issuecomment-1627994171"&gt;this comment&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I had to uninstall and reinstall Home Brew before it returned to work. It concerned the change from Mac Intel to Mac M1(Silicon).
See the article below from Josh Alletto to find out why. https://earthly.dev/blog/homebrew-on-m1/#:~:
text=On%20Intel%20Macs
%2C%20Homebrew%2C%20and,
%2Fusr%2Flocal%2Fbin%20.&amp;amp;text=
Homebrew%20chose%20%2Fusr
%2Flocal%2F,in%20your
%20PATH%20by%20default.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The link in the comment was a bit malformed, but I was able to clean it up and get this &lt;a href="https://earthly.dev/blog/homebrew-on-m1/"&gt;link&lt;/a&gt;. This is where I re-discovered&lt;sup id="sf-trying-out-pyenv-again-1-back"&gt;&lt;a href="#sf-trying-out-pyenv-again-1" class="simple-footnote" title="earlier in the day I was working through a post by Marijke about Caddy and there was a statement in her write up about how Homebrew on the M1 Macs stored files in a different directory, but when I ran the command to check where Homebrew was pointing I got the Intel location, not the Apple Silicon location ... this really should have been my first clue that some part of my set up was incorrect"&gt;1&lt;/a&gt;&lt;/sup&gt; that the way Homebrew is installed changed with the transition to the Apple Silicon.&lt;/p&gt;
&lt;p&gt;Now, I got a new M2 MacBook Pro in March 2023 and since I don't use Homebrew a lot AND I didn't really use pyenv for anything, I hadn't noticed that stuff kind of changed.&lt;/p&gt;
&lt;p&gt;Following the steps outlined I was able to redo my Homebrew and now have pyenv working. Now, the only question is will it's use 'stick' with me this time?&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-trying-out-pyenv-again-1"&gt;earlier in the day I was working through a &lt;a href="https://marijkeluttekes.dev/blog/articles/2024/03/25/custom-localhost-urls-with-caddyfile-on-macos/"&gt;post by Marijke about Caddy&lt;/a&gt; and there was a statement in her write up about how Homebrew on the M1 Macs stored files in a different directory, but when I ran the command to check where Homebrew was pointing I got the Intel location, not the Apple Silicon location ... this really should have been my first clue that some part of my set up was incorrect &lt;a href="#sf-trying-out-pyenv-again-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="python"></category><category term="pyenv"></category></entry><entry><title>Winter of Learning</title><link href="https://ryancheley.com/2024/03/19/winter-of-learning/" rel="alternate"></link><published>2024-03-19T00:00:00-07:00</published><updated>2024-03-19T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-03-19:/2024/03/19/winter-of-learning/</id><summary type="html">&lt;h2&gt;Winter of Learning Retrospective&lt;/h2&gt;
&lt;p&gt;Have you heard the good word about themes? If you haven't, take a look at this great video by &lt;a href="https://youtu.be/NVGuFdX5guE?si=auqXL9SMfYeftcup"&gt;CGP Grey on Themes&lt;/a&gt; and how they can work. For the last couple of years I've been doing yearly themes ... with limited success. This lack of success …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Winter of Learning Retrospective&lt;/h2&gt;
&lt;p&gt;Have you heard the good word about themes? If you haven't, take a look at this great video by &lt;a href="https://youtu.be/NVGuFdX5guE?si=auqXL9SMfYeftcup"&gt;CGP Grey on Themes&lt;/a&gt; and how they can work. For the last couple of years I've been doing yearly themes ... with limited success. This lack of success was entirely due to me not actually reviewing the status of my themes until the end of the year ... and by then it's too late!&lt;/p&gt;
&lt;p&gt;This last December I decided that I'd do the themes, but this time I'd do seasonal set of themes instead of one BIG annual theme.&lt;/p&gt;
&lt;p&gt;My current theme ended yesterday (March 18th) and this time I'm going to actually take stock of where I am and how 'well' I did.&lt;/p&gt;
&lt;p&gt;Since my theme started on December 21, 2023 which is the Northern Hemisphere Winter Solstice, I decided to have a seasonal theme of 'Winter of Learning' with the following things I wanted to learn more about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ryancheley/til/tree/main/tailscale"&gt;Tailscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ryancheley/til/tree/main/Docker"&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Postgres&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ryancheley/til/tree/main/css"&gt;CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To help me keep track of this I dusted off my &lt;a href="https://github.com/ryancheley/til"&gt;TIL github repo&lt;/a&gt; and started to write down some TILs. Over the course of the 88 days of my Seasonal theme I added 28 TILs. I also had 16 other, more personal, TILs that didn't make it into the repo for a total of 44 TILs. With 88 days that's a 50% hit rate on writing down stuff that I learned.&lt;/p&gt;
&lt;p&gt;This is much better than I thought I had done. I'd been pretty down on myself because I had meant to write a TIL every night, but I didn't. I over estimated the number of times I &lt;strong&gt;didn't&lt;/strong&gt; write a TIL and thought I had done much worse on it than I had.&lt;/p&gt;
&lt;p&gt;Now, just because I wrote a TIL doesn't mean that it was one of the topics above that I had indicated I would WANT to write about, but that's OK! The point of a TIL is to document some stuff that you learned and the topics above were only ever meant to be guides, not directives.&lt;/p&gt;
&lt;p&gt;I think the one thing I learned that I'm more proud of is spending a pretty good amount of time one weekend trying to learn Docker better. During one of &lt;a href="https://mastodon.social/deck/@webology"&gt;Jeff Triplett&lt;/a&gt;'s office hours I had joked that Docker scared me. And it was me actually saying it out loud that drove me to sit down and figure some shit out. I even had a &lt;a href="https://github.com/ryancheley/public-notes/issues/6"&gt;public notes issue&lt;/a&gt; about it!&lt;/p&gt;
&lt;p&gt;Overall this Winter of Learning isn't what I thought it would be, but I'm glad I did it. I am going to work to try and keep on writing TILs and hopefully I'll be able to get in at least 2 per week!&lt;/p&gt;
&lt;p&gt;That being said, it's now time to prepare for my next seasonal theme ... the Spring of Transition. My daughter is a Senior in High School and is getting ready to head off to college. Now seems like a good time to start getting ready for my wife and I to be empty nesters and so we'll be spending the next 92 days figuring out how we can do that.&lt;/p&gt;</content><category term="musings"></category><category term="themes"></category></entry><entry><title>An Argument to Realign the AHL</title><link href="https://ryancheley.com/2024/02/24/realign-the-ahl/" rel="alternate"></link><published>2024-02-24T00:00:00-08:00</published><updated>2024-02-24T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2024-02-24:/2024/02/24/realign-the-ahl/</id><summary type="html">&lt;h1&gt;What is the AHL&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://theahl.com/"&gt;The AHL&lt;/a&gt;, or American Hockey League, is a hockey minor league based in the US and Canada. It's widely considered to be the step right below the &lt;a href="https://www.nhl.com/"&gt;NHL&lt;/a&gt; which is the top Hockey League in North America.&lt;/p&gt;
&lt;p&gt;There are 32 teams in the AHL, and &lt;a href="https://theahl.com/qualification-rules"&gt;23 …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;h1&gt;What is the AHL&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://theahl.com/"&gt;The AHL&lt;/a&gt;, or American Hockey League, is a hockey minor league based in the US and Canada. It's widely considered to be the step right below the &lt;a href="https://www.nhl.com/"&gt;NHL&lt;/a&gt; which is the top Hockey League in North America.&lt;/p&gt;
&lt;p&gt;There are 32 teams in the AHL, and &lt;a href="https://theahl.com/qualification-rules"&gt;23 of them make the playoffs&lt;/a&gt;. The teams play for the &lt;a href="https://en.wikipedia.org/wiki/Calder_Cup"&gt;Calder Cup&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;What are the Calder Cup Playoffs?&lt;/h1&gt;
&lt;p&gt;The Calder Cup Playoffs are the name given to the AHL team vying for the Championship Calder Cup. The &lt;a href="https://en.wikipedia.org/wiki/2023_Calder_Cup_playoffs"&gt;2023 Calder Cup Playoffs&lt;/a&gt; started on April, 18 2023 with 14 teams playing in 7 different series while the other 9 all had byes, that is, they did NOT play in the first round.&lt;/p&gt;
&lt;p&gt;One thing to note is that the first round is essentially a play in&lt;sup id="sf-realign-the-ahl-1-back"&gt;&lt;a href="#sf-realign-the-ahl-1" class="simple-footnote" title="preliminary round that occurs before the start of the official playoffs. It is typically used to determine who gets the last spots in the playoff"&gt;1&lt;/a&gt;&lt;/sup&gt; round of the playoffs, but it's not an evenly divided play-in.&lt;/p&gt;
&lt;p&gt;The current format has round 1 with a best of Three Games, rounds 2 and 3 with a best of Five Games, and both the Conference Finals and Calder Cup Finals with a best of Seven Games.&lt;/p&gt;
&lt;h1&gt;Why Realign?&lt;/h1&gt;
&lt;p&gt;The ultimate goal is to enhance competitive balance and foster new rivalries, making the playoff race and outcomes more reflective of team performance throughout the season.&lt;/p&gt;
&lt;p&gt;The current structure of the AHL divides the entire league into 2 conferences, and each conference has 2 divisions.&lt;/p&gt;
&lt;p&gt;In the Eastern Conference, you have the Atlantic and North divisions. The Atlantic has 8 teams, while the North has 7.&lt;/p&gt;
&lt;p&gt;Out in the Western Conference, you have the Pacific and Central division. The Pacific has 10 teams, while the Central has 7 teams.&lt;/p&gt;
&lt;p&gt;Now an interesting thing about the playoffs is that the top 7 (of 10) teams from the Pacific make the playoffs, while the top 5 (of 7) teams from the North and Central make it. The Atlantic sends its top 6 (of 8) teams to the playoffs.&lt;/p&gt;
&lt;p&gt;Each division will have a certain number of teams with a bye-round, that is they don't have the play in the first round.&lt;/p&gt;
&lt;p&gt;In the North and Central three teams get a first-round bye, with only 2 teams playing in round 1. In the Atlantic three teams get a first-round bye with 4 teams playing in the first round.&lt;/p&gt;
&lt;p&gt;And in the Pacific division, you have 6 teams playing in the first round with only ONE team getting a first-round bye.&lt;/p&gt;
&lt;p&gt;So of the 23 teams that make the playoffs, 14 of them play in the first round, and of that 14, 6 come from the Pacific division.&lt;/p&gt;
&lt;p&gt;Seems a bit off to me.&lt;/p&gt;
&lt;p&gt;This also had the slightly embarrassing (for the AHL at least) aspect of seeing the second-best team in the entire league in the 2022-23 season (the Coachella Valley Firebirds) needing to win a Play-in round to make it into what might be considered the playoffs.&lt;/p&gt;
&lt;p&gt;By the time the Calder Cup Playoffs had concluded last year, the Firebirds lost to the Hershey Bears in 7 games. It was the MOST exciting series that I will ever get to see in person, or on TV.&lt;/p&gt;
&lt;p&gt;That being said, the Firebirds played 26 out of a possible 27 games during the playoffs last year. The Bears played in 20 out of a possible 24.&lt;/p&gt;
&lt;p&gt;If the Firebirds would have had a first-round bye, like the Bears, they would have most likely still played in 23 out of 24 games (3 fewer games than what they actually played) BUT three games can make a huge difference!&lt;/p&gt;
&lt;p&gt;I would like to make the case that realignment of the AHL, to a balanced set of divisions, and conferences is not only feasible, and easy, but in the best interests of the AHL.&lt;/p&gt;
&lt;p&gt;Before exploring the realignment scenario, I'll outline the proposed changes. The realignment aims to balance the divisions and conferences, ensuring an equal number of teams in each division and a fairer playoff qualification process.&lt;/p&gt;
&lt;p&gt;Additionally, if this realignment had happened last year, I believe that the outcomes could have been different (especially given the &lt;a href="https://en.wikipedia.org/wiki/Rose-colored_glasses"&gt;Firebird Colored Glasses&lt;/a&gt; I might be wearing).&lt;/p&gt;
&lt;p&gt;There is a post on the &lt;a href="https://theahl.com/howson-guiding-ahl-back-to-normalcy"&gt;AHL Site &lt;/a&gt; about realignment. Quoting the League President Scott Howson:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There’s no easy solution. I’m not saying it’ll never happen, but it’s not in the cards right now.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One item stated for a lack of realignment is, "Realignment would break up the Pacific cohort and likely force teams into the Central, and Howson does not see that as likely, given the additional travel burden that it could put on the division’s members."&lt;/p&gt;
&lt;p&gt;I'm going to assume this is NOT the case. What I have below is from a "Does this seem like it might work?" perspective. &lt;sup id="sf-realign-the-ahl-2-back"&gt;&lt;a href="#sf-realign-the-ahl-2" class="simple-footnote" title="A full analysis is something I'll be looking at for another post in the future. There are lots of other items to look at like (1) Schedules, (2) Travel, and (3) Rivalries"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h1&gt;Realignment&lt;/h1&gt;
&lt;p&gt;With my rationale for the need for realignment of the way, let's get into the actual implementation of the realignment.&lt;/p&gt;
&lt;p&gt;I built a &lt;a href="https://streamlit.io/"&gt;Streamlit app&lt;/a&gt; to see what a potential realignment would look like that can be found &lt;a href="https://ahl-realignment.streamlit.app/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The idea would be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Move both Tucson and Colorado to the Central Divsiion to get the Pacific Division down to 8 team&lt;/li&gt;
&lt;li&gt;Move Grand Rapids from the Central Division (in Western Conference) to the Atlantic Division (in the Eastern Conference) to get the Central and Atlantic to 8&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Impact of Realignment on the 2023 Calder Cup Finals&lt;/h2&gt;
&lt;p&gt;In the NHL there are 2 conferences with 2 divisions of 8 teams each. The playoffs get a total of 16 teams. The three top teams from each division, and then the top two teams from the Conference.&lt;/p&gt;
&lt;p&gt;This means that round 1 has 16 teams playing in 8 different series and NO ONE gets a bye.&lt;/p&gt;
&lt;p&gt;Let's imagine the 2023 Calder Cup Playoffs with realignment AND a similar style of playoff admission. One difference between the AHL and NHL I'd keep would be to have 5 games in round 1 and 2, and increase to 7 games in the Championship, versus the NHL which has 7 games in every round.&lt;/p&gt;
&lt;p&gt;I list the teams below in their proposed (potentially new) division. The number next to the team is the total points each team had at the end of the 2022-23 regular season.&lt;/p&gt;
&lt;p&gt;The Eastern Conference would have had the following seeding:&lt;/p&gt;
&lt;p&gt;Atlantic Division&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Providence (98)&lt;/li&gt;
&lt;li&gt;Hershey (97)&lt;/li&gt;
&lt;li&gt;Charlotte (86)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;North Division&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Toronto (90)&lt;/li&gt;
&lt;li&gt;Syracuse (81)&lt;/li&gt;
&lt;li&gt;Rochester (81)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Wild Card&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Springfield (84)&lt;/li&gt;
&lt;li&gt;Hartford (81)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Western Conference would have had the following seeding:&lt;/p&gt;
&lt;p&gt;Central Division&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Texas (92)&lt;/li&gt;
&lt;li&gt;Colorado (90)&lt;/li&gt;
&lt;li&gt;Milwaukee (89)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pacific Division&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calgary (106)&lt;/li&gt;
&lt;li&gt;Coachella Valley (103)&lt;/li&gt;
&lt;li&gt;Abbotsford (87)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Wild Card&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manitoba (84)&lt;/li&gt;
&lt;li&gt;Iowa (79)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Eastern Conference Playoffs&lt;/h3&gt;
&lt;h4&gt;Round 1 Best of Five games&lt;/h4&gt;
&lt;p&gt;In this section, we'll explore the first-round matchups, highlighting the top contenders and their paths to victory based on past performances and current strengths.&lt;/p&gt;
&lt;p&gt;Starting in the Eastern Conference we would have the following round 1 matchups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Providence (98) vs Hartford (81)&lt;/li&gt;
&lt;li&gt;Toronto (90) vs Springfield (84)&lt;/li&gt;
&lt;li&gt;Hershey (97) vs Charlotte (86)&lt;/li&gt;
&lt;li&gt;Syracuse (81) vs Rochester (81)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To determine the winners I'm going to look at the playoffs last year and then make an educated best guess as to the winners of each series&lt;/p&gt;
&lt;h5&gt;Providence vs Hartford&lt;/h5&gt;
&lt;p&gt;Providence lost to Hartford in the Atlantic division semi-finals last year, 1-3 and I see no reason for that to change ... other than maybe Hartford wins in 3 instead of 4. But I'm going to keep it at Hartford wins 3-1&lt;/p&gt;
&lt;h5&gt;Toronto vs Springfield&lt;/h5&gt;
&lt;p&gt;Toronto and Springfield didn't play each other in the playoffs last year or in the regular season. Springfield lost to Hartford 2-0 in round 1 while Toronto had a first-round bye and defeated Utica 3-1 in the North division semi-finals. My guess is that Toronto would have won this series 3-1&lt;/p&gt;
&lt;h5&gt;Hershey vs Charlotte&lt;/h5&gt;
&lt;p&gt;Hershey defeated Charlotte in the Atlantic division semi-finals last year 3-1. Granted Charlotte had just played 3 games against Lehigh Valley and won that series 2-1, I still think that Hershey defeats Charlotte, only it takes all 5 games. Hershey wins 3-2&lt;/p&gt;
&lt;h5&gt;Syracuse vs Rochester&lt;/h5&gt;
&lt;p&gt;Syracuse lost 3-2 to Rochester. Same result this time around I would think&lt;/p&gt;
&lt;h4&gt;Round 2 Best of Five games&lt;/h4&gt;
&lt;p&gt;Based on the seedings for the first round, I believe that Hartford would hold a higher position than Rochester&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hershey (97) vs Rochester (81)&lt;/li&gt;
&lt;li&gt;Toronto (90) vs Hartford (81)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Hershey vs Rochester&lt;/h5&gt;
&lt;p&gt;Hershey defeated Rochester 3-2 last year. I believe the outcome would be the same here (although this was in the Eastern Conference finals)&lt;/p&gt;
&lt;h5&gt;Toronto vs Hartford&lt;/h5&gt;
&lt;p&gt;Toronto and Hartford didn't play each other last year, though each team did get swept in the division finals. They played each other in the regular season only 2 times, each with the home team winning in OT. With Toronto getting the home nod, I'll extrapolate to say that Toronto wins in 5 games, 3-2&lt;/p&gt;
&lt;h4&gt;Round 3 (Eastern Conference Finals) Best of Seven&lt;/h4&gt;
&lt;h5&gt;Toronto vs Hershey&lt;/h5&gt;
&lt;p&gt;Toronto played Hershey 2 times and lost both times. I think that a series like this would be closer, but Hershey comes out on top 4-2&lt;/p&gt;
&lt;h4&gt;Eastern Conference Champion Review&lt;/h4&gt;
&lt;p&gt;In this realigned AHL for the Calder Cup finals, Hershey has played 16 out of 17 games, going 10-6 to reach the Calder Cup Finals. When they actually reached the Calder Cup finals last year, they played only 13 games going 10-3 (playing an extra 3 games)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Providence (98)----|
                   |--Hartford (81)----|
Hartford (81)------|                   |
                                       |--Toronto (90)-----|
Toronto (90)-------|                   |                   |
                   |--Toronto (90)-----|                   |
Springfield (84)---|                                       |
                                                           |--Hershey (87)
Hershey (87)-------|                                       |
                   |--Hershey (87)-----|                   |
Charlotte (86)-----|                   |                   |
                                       |--Hershey (87)-----|
Syracuse (81)------|                   |
                   |--Rochester (81)---|
Rochester (81)-----|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's take a look out West next&lt;/p&gt;
&lt;h2&gt;Western Conference Playoffs&lt;/h2&gt;
&lt;h3&gt;Round 1 Best of Five games&lt;/h3&gt;
&lt;p&gt;In the Western conference we would have had the following round 1 matchups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calgary (106) vs Iowa (79)&lt;/li&gt;
&lt;li&gt;Texas (92) vs Manitoba (84)&lt;/li&gt;
&lt;li&gt;Coachella Valley&lt;sup id="sf-realign-the-ahl-3-back"&gt;&lt;a href="#sf-realign-the-ahl-3" class="simple-footnote" title="Coachella Valley is abbreviated CV in some cases below to save space"&gt;3&lt;/a&gt;&lt;/sup&gt; (103) vs Abbotsford (87)&lt;/li&gt;
&lt;li&gt;Colorado (90) vs Milwaukee (89)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As with the Eastern Conference bracket to determine the winners I'm going to look at the playoffs last year and then make an educated best guess as to the winners of each series.&lt;/p&gt;
&lt;h5&gt;Calgary vs Iowa&lt;/h5&gt;
&lt;p&gt;Calgary didn't play Iowa at all in either the regular season or the postseason last year. Based on the final records, I think it's safe to say that Calgary wins this series, but I believe it's 3-1&lt;/p&gt;
&lt;h5&gt;Texas vs Manitoba&lt;/h5&gt;
&lt;p&gt;Texas played Manitoba 8 times in the regular season last year with Manitoba winning 6 of these games. There were several that went into OT which Manitoba won more often than not. I think this goes to 5 games, but Manitoba wins 3-2.&lt;/p&gt;
&lt;h5&gt;Coachella Valley vs Abbotsford&lt;/h5&gt;
&lt;p&gt;Coachella Valley and Abbotsford played 4 times, each winning two games. Given the disparity in total points at the end of the year, I think that Coachella Valley wins in five 3-2.&lt;/p&gt;
&lt;h5&gt;Colorado vs Milwaukee&lt;/h5&gt;
&lt;p&gt;Colorado and Milwaukee didn't play each other at all in the regular season. I think that this is an even match-up, but I give the edge to Colorado since they're the home team, winning 3-2.&lt;/p&gt;
&lt;h3&gt;Round 2 Best of Five games&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Calgary (106) vs Manitoba (84)&lt;/li&gt;
&lt;li&gt;Coachella Valley (103) vs Colorado (90)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Calgary vs Manitoba&lt;/h5&gt;
&lt;p&gt;Calgary wins this in 5, 3-2. They didn't play each other at all during the regular season, but I think that provincial pride forces the series to 5 games&lt;/p&gt;
&lt;h5&gt;Coachella Valley vs Colorado&lt;/h5&gt;
&lt;p&gt;Coachella Valley wins this in 5, 3-2. This is a repeat of the series last year. I believe that it goes the distance again.&lt;/p&gt;
&lt;h3&gt;Round 3 (Western Conference Finals) Best of Seven&lt;/h3&gt;
&lt;h5&gt;Calgary vs Coachella Valley&lt;/h5&gt;
&lt;p&gt;Coachella Valley defeats Calgary 4-3&lt;/p&gt;
&lt;p&gt;Coachella Valley and Calgary played each other in the Pacific Division Finals last year. It was a brutal series with Coachella Valley winning in game 5 in Overtime. I think that the same result comes out here going the distance.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Calgary (106)------|
                   |--Calgary (106)------|
Iowa (79)----------|                     |
                                         |--Calgary (106)----|
Texas (92)---------|                     |                   |
                   |--Manitoba (84) -----|                   |
Manitoba (84)------|                                         |
                                                             |--CV (103)
CV (103)-----------|                                         |
                   |--CV (103)-----------|                   |
Abbotsford (87)----|                     |                   |
                                         |--CV (103)---------|
Colorado (90)------|                     |
                   |--Colorado (90)------|
Milwaukee (89)-----|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Western Conference Champion Review&lt;/h3&gt;
&lt;p&gt;At the Calder Cup finals, Coachella Valley has played 17 out of 17 games, going 10-7. When they actually reached the Calder Cup finals last year, they played 19 games going 12-7. Two fewer games to get here.&lt;/p&gt;
&lt;h3&gt;Calder Cup Finals&lt;/h3&gt;
&lt;p&gt;In this matchup, Coachella Valley will have up to 4 home games, while Hershey will have up to 3.&lt;/p&gt;
&lt;p&gt;Coachella Valley played in 2 fewer games in this scenario, while Hershey played in 3 more games, for a net difference of 5 games.&lt;/p&gt;
&lt;p&gt;I think the first two games go as they did last year. Epic drubbings at Acrisure Arena by Coachella Valley over Hershey. I think that the 3 games&lt;sup id="sf-realign-the-ahl-4-back"&gt;&lt;a href="#sf-realign-the-ahl-4" class="simple-footnote" title="In all honesty the 3 games in Hershey last year were games where the Firebirds seemed a bit tired. There was also some officiating that seemed a bit dubious (an offside that didn't get called that led to a goal that, IMO, shouldn't have counted)"&gt;4&lt;/a&gt;&lt;/sup&gt; in Hershey go 2-1 in favor of Hershey giving the Firebirds a chance to clinch on home ice in game 6 which they do.&lt;/p&gt;
&lt;h2&gt;Evaluating the Impact of Realignment: A Reflection on Competitive Balance and Missed Opportunities&lt;/h2&gt;
&lt;p&gt;Going through this exercise doesn't change the outcome of the 2023 Calder Cup Finals. And I don't want this to seem like a sour grapes sort of thing. The Hershey Bears won the Final game last year within the confines of the structure that was set up by the AHL. In that sense, they won it fair and square.&lt;/p&gt;
&lt;p&gt;Also, I'm not sure if this realignment had been in place my predictions would have been correct necessarily.&lt;/p&gt;
&lt;p&gt;What I think it does point out is an extreme disadvantage that the Pacific division faces in the playoffs. Last year the Coachella Valley Firebirds were the FIRST team west of Austin to make the Finals. They were also the first team to EVER play a playoff game in EVERY timezone that the AHL operates a team in.&lt;/p&gt;
&lt;h2&gt;Coda&lt;/h2&gt;
&lt;p&gt;I was bummed that the Firebirds lost in Game 7 last year. &lt;a href="https://www.ryancheley.com/2023/07/01/firebirds-inaugural-season/"&gt;I wrote about it&lt;/a&gt; just a few days after it happened.&lt;/p&gt;
&lt;p&gt;It was the most exciting sporting event I've ever seen, either in person or on TV. I'm not sure anything will ever be that intense and exciting.&lt;/p&gt;
&lt;p&gt;I really wish they would have won, and this shows that they just might have been able to if the conference, divisions, and playoff seedings were a bit more balanced.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-realign-the-ahl-1"&gt;preliminary round that occurs before the start of the official playoffs. It is typically used to determine who gets the last spots in the playoff &lt;a href="#sf-realign-the-ahl-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-realign-the-ahl-2"&gt;A full analysis is something I'll be looking at for another post in the future. There are lots of other items to look at like (1) Schedules, (2) Travel, and (3) Rivalries &lt;a href="#sf-realign-the-ahl-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-realign-the-ahl-3"&gt;Coachella Valley is abbreviated CV in some cases below to save space &lt;a href="#sf-realign-the-ahl-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-realign-the-ahl-4"&gt;In all honesty the 3 games in Hershey last year were games where the Firebirds seemed a bit tired. There was also some officiating that seemed a bit dubious (an offside that didn't get called that led to a goal that, IMO, shouldn't have counted) &lt;a href="#sf-realign-the-ahl-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="ahl"></category><category term="hockey"></category></entry><entry><title>Year in Review 2023</title><link href="https://ryancheley.com/2023/12/31/year-in-review-2023/" rel="alternate"></link><published>2023-12-31T00:00:00-08:00</published><updated>2023-12-31T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-12-31:/2023/12/31/year-in-review-2023/</id><summary type="html">&lt;p&gt;I've never done a year in review, but this seems like a good a time as any, right? I had a rough outline, but after reading the great Year in Review from &lt;a href="https://www.better-simple.com/personal/2023/12/30/my-year-in-review/"&gt;Tim Schilliing&lt;/a&gt;, &lt;a href="https://www.paulox.net/2023/12/31/my-2023-in-review/"&gt;Paolo Melichore&lt;/a&gt;, and &lt;a href="https://dev.to/veldakiara/djangoconus-2023-a-wish-fulfilled-2mmc"&gt;Velda Kiara&lt;/a&gt;, I was inspired to &lt;strong&gt;actually&lt;/strong&gt; finish mine.&lt;/p&gt;
&lt;h1&gt;Professional&lt;/h1&gt;
&lt;p&gt;In the moment …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've never done a year in review, but this seems like a good a time as any, right? I had a rough outline, but after reading the great Year in Review from &lt;a href="https://www.better-simple.com/personal/2023/12/30/my-year-in-review/"&gt;Tim Schilliing&lt;/a&gt;, &lt;a href="https://www.paulox.net/2023/12/31/my-2023-in-review/"&gt;Paolo Melichore&lt;/a&gt;, and &lt;a href="https://dev.to/veldakiara/djangoconus-2023-a-wish-fulfilled-2mmc"&gt;Velda Kiara&lt;/a&gt;, I was inspired to &lt;strong&gt;actually&lt;/strong&gt; finish mine.&lt;/p&gt;
&lt;h1&gt;Professional&lt;/h1&gt;
&lt;p&gt;In the moment it can feel like I don't really get anything done at work. Looking at my &lt;a href="https://track.toggl.com/shared-report/9091b753451ad2edafbb36f18be33d82/summary/period/last12Months"&gt;time tracking stats&lt;/a&gt;, I do spend A LOT of my time in meetings (nearly 40%) and administration (almost 45%) which is expected for someone in management I suppose, but I really do miss getting to write code more often.&lt;/p&gt;
&lt;p&gt;That being said I was able to complete some pretty significant projects at work with the help of my team that I'm really proud of.&lt;/p&gt;
&lt;h2&gt;Migrations&lt;/h2&gt;
&lt;p&gt;Change is hard, and we underwent a few BIG technology changes that have gone really well.&lt;/p&gt;
&lt;p&gt;The first big change implemented was to migrate from a few Atlassian products (&lt;a href="https://en.wikipedia.org/wiki/Jira_(software)"&gt;JIRA&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Confluence_(software)"&gt;Confluence&lt;/a&gt;) to &lt;a href="https://en.wikipedia.org/wiki/YouTrack"&gt;YouTrack&lt;/a&gt;. I know there are lots of people out there that HATE JIRA, but I loved it and my team liked it. I think that a big reason for that is when JIRA wasn't doing what we needed it to do, I was able to make changes to it. We didn't have to pass it through some change control committee, or get buy in from some high level manager. We just made it work for us ... and it really did work well for us.&lt;/p&gt;
&lt;p&gt;The reason we had to migrate from these products was that Atlassian announced in February of 2021 that they would end-of-life the server versions at the end of February 2024. I looked to see if we could migrate to one of their data center versions, but because I'm in Health Care any solution 'in the cloud' needs to be HIPAA compliant. While Atlassian does offer &lt;a href="https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act"&gt;HIPAA&lt;/a&gt; compliant versions, you need to have 500+ users for that solution. My organization has 50.&lt;/p&gt;
&lt;p&gt;I spent two years trying to figure out how we could keep JRIA and/or to find something that could replace what we had in JIRA and the best solution I could find was JetBrains' YouTrack.&lt;/p&gt;
&lt;p&gt;We've been on YouTrack since the end of May and while there are still some features that I miss (support for &lt;a href="https://mermaid.js.org/"&gt;Mermaid Diagrams&lt;/a&gt;, ability to embed the content of one Confluence Article into another Article, automatic linking between JIRA issues and Confluence Articles) overall the workflow parts of YouTrack for issue tracking are much better than JIRA. Easier to set up, easier to maintain.&lt;/p&gt;
&lt;p&gt;Another change that we made was changing our &lt;a href="https://en.wikipedia.org/wiki/Version_control"&gt;Version Control System&lt;/a&gt; from &lt;a href="https://en.wikipedia.org/wiki/Apache_Subversion"&gt;Subversion&lt;/a&gt; to &lt;a href="https://en.wikipedia.org/wiki/Git"&gt;git&lt;/a&gt;, hosted on Azure DevOps. This involved all three of the teams in my department and proceeded in a staged approach over the course of about 3 months. I also helped another department migrate from Subversion to git.&lt;/p&gt;
&lt;p&gt;The biggest challenge was the &lt;a href="https://en.wikipedia.org/wiki/SQL_Server_Integration_Services"&gt;SSIS&lt;/a&gt; packages used in our &lt;a href="https://en.wikipedia.org/wiki/Extract,_transform,_load"&gt;ETL&lt;/a&gt; processes, and the database objects.&lt;/p&gt;
&lt;p&gt;The SSIS packages took 3 attempts before it stuck, but the ETL devs were positive with each unsuccessful attempt and we finally got over the hump in early December.&lt;/p&gt;
&lt;p&gt;The Database objects are unfortunately still in Subversion. This is a limitation of our current tech stack. Migrating to git requires that each developer have their own version of the database but we don't. Honestly the way we have it set up now is something I'd really like to change, but that's a story for a different time.&lt;/p&gt;
&lt;p&gt;In all we migrated 25 repositories from Subversion. There is still more work to do with the Web Developers to update our CICD process to fully leverage Azure DevOps, but small steps can make for big changes over time. No need to rush if we have a working CICD system (even if it's kind of Frankensteined together at this point).&lt;sup id="sf-year-in-review-2023-1-back"&gt;&lt;a href="#sf-year-in-review-2023-1" class="simple-footnote" title="Our current stack involves commits to Azure DevOps which is picked up by TeamCity and then deployed using  Octopus Deploy"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;With this migration to git we were also able to integrate our issue tracking system (YouTrack) with our VCS. It's nice to see commits automatically 'connected' to the issues in YouTrack.&lt;/p&gt;
&lt;p&gt;Another thing that I've been able to work on is getting more and more Python enabled for various projects. We have a Django App that we use to manage 'administrative' tables in our MS SQL database, and we've been able to integrate Python in some of our SSIS packages for ETL.&lt;/p&gt;
&lt;h2&gt;Speaking&lt;/h2&gt;
&lt;p&gt;One of the goals that I had from my last annual review was to engage in two public speaking activities. While I give lots of presentations at work, they're all via Zoom so the idea of getting up in a room full of strangers and talking was both exciting and terrifying.&lt;/p&gt;
&lt;p&gt;The first conference I spoke at was the KLAS Points of Light conference in May in Salt Lake City (only about a week after PyCon US). The talk was limited to 10 minutes and I had 2 co-speakers so I was limited to about 3 minutes of talking time (and if I said I spoke for 90 seconds that would be pretty generous). That being said, I did get up on stage and spoke to a room full of about 200 strangers (and nearly threw up!)&lt;/p&gt;
&lt;p&gt;The absolute highlight of my speaking engagements this year was speaking at Django Con (which I wrote about &lt;a href="https://www.ryancheley.com/2023/10/24/djangocon-us-2023/"&gt;here&lt;/a&gt; and &lt;a href="https://www.ryancheley.com/2023/12/15/so-you-want-to-give-a-talk-at-a-conference/"&gt;here&lt;/a&gt;). I won't write more about it, but I had such a great time giving that talk!&lt;/p&gt;
&lt;h2&gt;Certifications&lt;/h2&gt;
&lt;p&gt;I was able to achieve a couple of certifications this year. The first was the Google Cloud Platform Cloud Architect Certificate. I wrote about the experience &lt;a href="https://www.ryancheley.com/2023/04/01/gcp-cloud-architect-exam-experience/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Another certification I achieved was the Certified EDI Academy Professional. Initially I did this mostly because the cost of the classes to work on the certificate for 2 participants versus 3 participants was only $100 extra and there were 2 people in my department that had asked about working on the certification. Since my department is in charge of EDI 'stuff' and I'm in charge of the department it kind of made sense that I should get it too.&lt;/p&gt;
&lt;p&gt;While I didn't think it would be super beneficial and did it mostly &lt;em&gt;just because&lt;/em&gt; I have been surprised at how useful it's ended up being. Seeing what's possible with EDI in Healthcare has allowed me to work with the EDI Analysts in my department more effectively AND has helped us all to better identify opportunities for automation&lt;/p&gt;
&lt;h2&gt;Misc&lt;/h2&gt;
&lt;p&gt;Above I lamented the lack of time to program above, but one thing I was able to work on was a refactor of an Airflow DAG from 2000+ lines down to 150 lines. This was thanks to the DjangoCon Tutorial &lt;a href="https://2023.djangocon.us/tutorials/django-3-airflow/"&gt;Django ❤️ Airflow&lt;/a&gt; lead by &lt;a href="https://fosstodon.org/@sheena"&gt;Sheena O'Connell&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is also the first year since the start of the pandemic that I've gone into the office on a (mostly) regular basis. While it's mostly like working from my home office (lots of Zoom meetings) it is nice to have a different bit of scenery (the new arena where hockey is played is visible from my desk when I look out the window).&lt;/p&gt;
&lt;p&gt;In November I also got my first promotion in 7 years which was nice. I went from being the Regional Director to Senior Regional Director.&lt;/p&gt;
&lt;p&gt;Finally something I was really excited and proud about was the rating my management team and I got for Employee Satisfaction. This was the first full year that I had two people other than me in the management team and I think that really helped. The satisfaction rating came back at 95%, the highest my department has ever gotten.&lt;/p&gt;
&lt;h1&gt;Personal&lt;/h1&gt;
&lt;h2&gt;Health&lt;/h2&gt;
&lt;p&gt;At the end of last year I completed the Running Challenge which lead to me participating in my first organized run (the Panther 5K) since 2018&lt;sup id="sf-year-in-review-2023-2-back"&gt;&lt;a href="#sf-year-in-review-2023-2" class="simple-footnote" title="That year I ran the LA Marathon in March, and in July I tore a muscle in my left hamstring"&gt;2&lt;/a&gt;&lt;/sup&gt;. I had hoped that this would get me back into running and that by the end of 2023 I would have been able to run a half marathon.&lt;/p&gt;
&lt;p&gt;These hopes were dashed in April when I contracted COVID (for the second time since the start of the pandemic) and I wasn't back to feeling like myself until late May. Now, in most places of the country late May might be a swell time to start running, but in the Coachella Valley it's already push triple digit highs so I had a hard time getting motivated to start running again when it was that hot.&lt;/p&gt;
&lt;p&gt;I started the Running Challenge again this year, but 24 days into it I got a really bad cold that basically is only now (nearly 2 weeks later) truly disappearing. I haven't run in those 2 weeks, but am looking forward to starting &lt;a href="https://www.901pt.com/post/rucking-what-it-is-benefits-how-to-do-it"&gt;rucking&lt;/a&gt; and then running again in 2024.&lt;/p&gt;
&lt;h2&gt;Django&lt;/h2&gt;
&lt;p&gt;I mentioned above that I spoke at DjangoCon US this year in Durham, but before the conference started I got to see my youngest step brother and his wife at their (new to me) house&lt;sup id="sf-year-in-review-2023-3-back"&gt;&lt;a href="#sf-year-in-review-2023-3" class="simple-footnote" title="They've been in the house for almost 9 years!"&gt;3&lt;/a&gt;&lt;/sup&gt;. It was a great way to start an amazing week in Durham which is one of the more walkable cities I've been to.&lt;/p&gt;
&lt;p&gt;Another bonus was a chance encounter with &lt;a href="https://www.linkedin.com/in/ronardluna/"&gt;Ronard Luna&lt;/a&gt; (whom I met at DCUS 2022 in San Diego) and some of his Caktus colleagues after day one of the conference. We went and got (really good) Thai that night, had some great conversations and I got to meet some more amazing Django people.&lt;/p&gt;
&lt;p&gt;Towards the end of the conference &lt;a href="https://mastodon.social/@kjaymiller"&gt;Jay Miller&lt;/a&gt; interviewed me about my talk and that was super awesome. I was nervous at first, but Jay (and &lt;a href="https://mastodon.online/@BajoranEngineer"&gt;Dawn&lt;/a&gt;) did a great job of making me feel at ease 😁&lt;/p&gt;
&lt;p&gt;I also spent time working with &lt;a href="https://mastodon.social/@webology"&gt;Jeff Triplett&lt;/a&gt; and &lt;a href="https://github.com/saadmk11"&gt;Maksudul Haque&lt;/a&gt; on &lt;a href="https://djangopackages.org"&gt;DjangoPackages&lt;/a&gt; which has been fun and a great learning experience. I'm looking forward to continuing that work next year!&lt;/p&gt;
&lt;p&gt;Finally, towards the end of the year I interviewed and was accepted to be one of the &lt;a href="https://djangonaut.space/"&gt;Djangonaut.Space&lt;/a&gt; Navigators. I'm really looking forward to working with the Djangonauts on my team, as well as my Captain Nishant Aggarwal.&lt;/p&gt;
&lt;h2&gt;Reading&lt;/h2&gt;
&lt;p&gt;I had a goal of increasing the diversity (both in style and authors) that I was going to read this year&lt;sup id="sf-year-in-review-2023-4-back"&gt;&lt;a href="#sf-year-in-review-2023-4" class="simple-footnote" title="I read mostly Sci Fi written by people that mostly look like me"&gt;4&lt;/a&gt;&lt;/sup&gt;. To this end my daughter Abby helped me by putting together a list of books by Author's to get me out of my reading rut.&lt;/p&gt;
&lt;p&gt;I kind of fell off the reading wagon in the last quarter of the year, but I was able to read some really good books that I wouldn't have found otherwise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;American Gods: Neil Gaiman&lt;/li&gt;
&lt;li&gt;Scythe: Neal Shusterman&lt;/li&gt;
&lt;li&gt;Renegads: Marissa Meyer&lt;/li&gt;
&lt;li&gt;Don't Read the Comments: Eric Smith&lt;/li&gt;
&lt;li&gt;An Absolutely Remarkable Thing: Hank Green&lt;/li&gt;
&lt;li&gt;The Thousandth Floor: Katherine McGee&lt;/li&gt;
&lt;li&gt;Legendborn: Tracy Deonn&lt;/li&gt;
&lt;li&gt;Mistborn: Brandon Sanderson&lt;/li&gt;
&lt;li&gt;War Girls: Tochi Onyebuchi&lt;/li&gt;
&lt;li&gt;The Poppy War: RF Kuang&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also read a few books in the Rise of Mankind Series by John Walker&lt;sup id="sf-year-in-review-2023-5-back"&gt;&lt;a href="#sf-year-in-review-2023-5" class="simple-footnote" title="These aren't particular good or well written, but I was in between books and they were on my kindle so 🤷🏼‍♂️"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Raid&lt;/li&gt;
&lt;li&gt;Conflict&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Writing&lt;/h2&gt;
&lt;p&gt;I only wrote &lt;a href="https://search-ryancheley.vercel.app/pelican?sql=select+summary+as+%27Summary%27%2C+url+as+%27URL%27%2C+published_date+as+%27Published+Data%27+%0D%0Afrom+content+%0D%0Awhere+published_date+%3E%3D+%272023-01-01%27+%0D%0Aand+category+%21%3D+%27pages%27%0D%0Aorder+by+published_date"&gt;nine articles this year&lt;/a&gt; (including this one). It sure feels like more, but in looking back I didn't write my first post until April, and then not again until July. It was really in the last 3 months (since DjangoCon) that I really started to write more with 2 in October and November and three in December.&lt;/p&gt;
&lt;p&gt;I'm looking forward to writing more in 2024 with the goal of one article per month. I've started already with trying to write up one &lt;a href="https://github.com/ryancheley/til"&gt;TIL&lt;/a&gt; a day. This is part of a large theme&lt;sup id="sf-year-in-review-2023-6-back"&gt;&lt;a href="#sf-year-in-review-2023-6" class="simple-footnote" title="more on that in the next article 😁"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Hockey&lt;/h2&gt;
&lt;p&gt;On December 18, 2022 AHL Hockey made its way to my home town. The best part is that the arena they play in is only 10 minutes from my house so I went to &lt;em&gt;a lot&lt;/em&gt; of hockey games.&lt;/p&gt;
&lt;p&gt;So far this season isn't going like I had hoped, but a few highlights from last season were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Getting to see a triple overtime game against the Calgary Wranglers that ended with the Firebirds winning&lt;/li&gt;
&lt;li&gt;A game 7 of the Calder Cup finals going to over time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While the 3OT game ended with the good guys winning, the game 7 OT ended with them losing. It was heart breaking, and I wrote about it &lt;a href="https://www.ryancheley.com/2023/07/01/firebirds-inaugural-season/"&gt;here&lt;/a&gt;, so I won't go over it again. That being said, even though they lost, the fact that I got to go to a Game 7 for a championship was already mind blowing. The fact that it went into overtime was more so. I did a bit a research and it was the first Game 7 OT championship game in either the AHL or NHL since the early 50s, so it was kind of neat to be a part of history.&lt;/p&gt;
&lt;p&gt;I've gotten so into the AHL that I've written &lt;a href="https://github.com/ryancheley/ahl"&gt;a silly scraper&lt;/a&gt; that dumps data into a &lt;a href="https://datasette.io"&gt;datasette&lt;/a&gt; &lt;a href="https://ahl-data.vercel.app"&gt;instance on vercel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At the time of this writing the Firebirds are &lt;a href="https://ahl-data.vercel.app/games?sql=with+data+%28TheYear%2C+W%2C+L%2C+OTL%2C+SOL%29%0D%0Aas+%28%0D%0A%0D%0Aselect+strftime%28%27%25Y%27%2C+game_date%29%0D%0A%2C+sum%28case%0D%0A++when+home_team+%3D+%3Ateam_name+and+home_team_score+%3E+away_team_score+then+1%0D%0A++when+away_team+%3D+%3Ateam_name+and+home_team_score+%3C+away_team_score+then+1%0D%0A++else+0%0D%0Aend%29+as+%27W%27%0D%0A%2C+sum%28case%0D%0A++when+home_team+%3D+%3Ateam_name+and+home_team_score+%3C+away_team_score+and+game_status+%3D+%27Final%27+then+1%0D%0A++when+away_team+%3D+%3Ateam_name+and+home_team_score+%3E+away_team_score+and+game_status+%3D+%27Final%27then+1%0D%0A++else+0%0D%0Aend%29+as+%27L%27%0D%0A%2C+sum%28case%0D%0A++when+home_team+%3D+%3Ateam_name+and+home_team_score+%3C+away_team_score+and+game_status+%3D+%27Final+OT%27+then+1%0D%0A++when+away_team+%3D+%3Ateam_name+and+home_team_score+%3E+away_team_score+and+game_status+%3D+%27Final+OT%27then+1%0D%0A++else+0%0D%0Aend%29+as+%27OTL%27%0D%0A%2C+sum%28case%0D%0A++when+home_team+%3D+%3Ateam_name+and+home_team_score+%3C+away_team_score+and+game_status+%3D+%27Final+SO%27+then+1%0D%0A++when+away_team+%3D+%3Ateam_name+and+home_team_score+%3E+away_team_score+and+game_status+%3D+%27Final+SO%27then+1%0D%0A++else+0%0D%0Aend%29+as+%27SOL%27%0D%0Afrom%0D%0A++games%0D%0Awhere+%28home_team+%3D+%3Ateam_name%0D%0A+++++++or+away_team+%3D+%3Ateam_name%29%0D%0Aand+++strftime%28%27%25m-%25d%27%2C+game_date%29+%3E%3D+%2710-01%27%0D%0A++AND+game_date+%3C%3D+strftime%28%27%25Y%27%2C+game_date%29+%7C%7C+%27-%27+%7C%7C+strftime%28%27%25m-%25d%27%2C+%27now%27%29%0D%0A++group+by+strftime%28%27%25Y%27%2C+game_date%29%0D%0A%29%0D%0Aselect+*%0D%0A%2C+2+*+W+%2B+OTL+%2B+SOL+as+%27Points%27%0D%0Afrom+data%0D%0Aorder+by+TheYear&amp;amp;team_name=Coachella+Valley+Firebirds&amp;amp;_hide_sql=1"&gt;9 points behind the pace they had last year&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With that, it's still pretty awesome that I get to watch hockey live a couple of times a week and don't have to travel hours to do it.&lt;/p&gt;
&lt;h1&gt;House&lt;/h1&gt;
&lt;p&gt;When my wife Emily and I bought our house in 2009 we were surprised that it was on septic and not connected to the sewer. But then we learned that the unincorporated part of the county we live in that's not unusual. Every few years I call one of the &lt;a href="https://hammerplumbing.com/"&gt;local plumbing companies&lt;/a&gt; that is highly regarded to empty my septic tank.&lt;/p&gt;
&lt;p&gt;This was the year to have the tank emptied and when they came out to empty it, we discovered that the tank was collapsing on itself and would need to be replaced.&lt;/p&gt;
&lt;p&gt;Now, this is not an inexpensive expense&lt;sup id="sf-year-in-review-2023-7-back"&gt;&lt;a href="#sf-year-in-review-2023-7" class="simple-footnote" title="Average costs is about $15,000"&gt;7&lt;/a&gt;&lt;/sup&gt; but also not totally unexpected. What was unexpected was to find out that because our house was within 200 feet of the sewer line we were REQUIRED to connect to the sewer.&lt;/p&gt;
&lt;p&gt;After contacting 12 approved contractors we were able to get one under contract and they got us connected to the sewer. It cost WAAAAAY more than I think anything should&lt;sup id="sf-year-in-review-2023-8-back"&gt;&lt;a href="#sf-year-in-review-2023-8" class="simple-footnote" title="Close to $30,000"&gt;8&lt;/a&gt;&lt;/sup&gt;, but it's done now so one less thing to worry about going forward&lt;/p&gt;
&lt;p&gt;But the silver lining in that is I finally felt comfortable getting a lemon tree in my front yard and it brings me lots of joy. &lt;sup id="sf-year-in-review-2023-9-back"&gt;&lt;a href="#sf-year-in-review-2023-9" class="simple-footnote" title="When Emily and I were looking to buy a house we only had three requirements: (1) It couldn't be behind a gate; (2) it couldn't have a pool; (3) it had to have a citrus tree, preferably lemon. We were able to get 2 of the three when we bought the house and it only took 13 years to get the citrus tree!"&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h1&gt;Family&lt;/h1&gt;
&lt;p&gt;This fall my daughter Abby started her Senior year in High School. This is a mind blowing stage in life. It means that this time next year Emily and I will officially be empty nesters.&lt;/p&gt;
&lt;p&gt;In preparation for the transition to College we have done a lot of College tours. These have mostly been short weekend trips, but it's been nice to get out there and visit new / different places.&lt;/p&gt;
&lt;p&gt;Before the pandemic my family and I would take a &lt;a href="https://www.imdb.com/title/tt0085995/"&gt;stereotypical American style family road trip&lt;/a&gt;. We haven't done it since, but we were hoping to do something big this summer.&lt;/p&gt;
&lt;p&gt;Those plans were derailed when the sewer bill came in, but the college tours, and a nice long weekend trip to &lt;a href="https://visitjulian.com/"&gt;Julian&lt;/a&gt; made up for the lack of a BIG trip.&lt;/p&gt;
&lt;p&gt;I mentioned above the Hockey games I've been able to see at Acrisure Arena, but one of the extra benefits of having an arena where they play hockey is that they will also play music. I was only able to go to one concert (&lt;a href="https://en.wikipedia.org/wiki/Paramore"&gt;Paramore&lt;/a&gt; with Abby), but Emily and Abby were able to see several shows including &lt;a href="https://www.shaniatwain.com/#/"&gt;Shania Twain&lt;/a&gt;, &lt;a href="https://www.lizzomusic.com/"&gt;Lizzo&lt;/a&gt;, and &lt;a href="https://www.ptxofficial.com/"&gt;Pentatonix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We also live relatively close to LA so we were able to see a couple of events at the Staples Center (I refuse to call it by it's new name) including &lt;a href="https://www.szasos.com/tour/"&gt;SZA&lt;/a&gt; (all three of us plus a friend of Abby's) and a &lt;a href="https://www.nhl.com/avalanche/news/colorado-avalanche-los-angeles-kings-game-recap-december-3-x2816"&gt;Kings game&lt;/a&gt; (just Emily and me).&lt;/p&gt;
&lt;p&gt;Abby was also able to see the last show of &lt;a href="https://www.taylorswift.com/tour-us/"&gt;Taylor Swift's Eras tour&lt;/a&gt; at &lt;a href="https://en.wikipedia.org/wiki/SoFi_Stadium"&gt;SoFi Stadium&lt;/a&gt; which was a bit stressful as she did it with a group of friends and an adult cousin of one of those friends (that we didn't know) but she had a great time and had a smile as big as any I've seen on her in a while for a few days after.&lt;/p&gt;
&lt;p&gt;Emily and I also went down to the Palm Springs Pride parade and got to see &lt;a href="https://maniacs.com/"&gt;10,000 Maniacs&lt;/a&gt; with their new lead singer (&lt;a href="https://en.wikipedia.org/wiki/Leigh_Nash"&gt;Leigh Nash&lt;/a&gt; from &lt;a href="https://en.wikipedia.org/wiki/Sixpence_None_the_Richer"&gt;Six Pence None the Richer&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;We have also really started to take advantage of the space in our back yard as a family. As a 15 year work anniversary gift I received a projector TV that we've set up outside. We also got a fire pit to keep us warm in the &lt;em&gt;frigid&lt;/em&gt; Desert Winter Nights (I mean, it gets down to a low of like 50 by the time I go back inside 🥶) and reminds me of &lt;a href="https://imgur.com/tczZ7ez"&gt;this meme&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Tropical Storm Hillary&lt;/h1&gt;
&lt;p&gt;I grew up in the Coachella Valley, and except for a 10 year period (mostly in my 20s) I've lived here my entire life. I've seen &lt;a href="https://en.wikipedia.org/wiki/Haboob"&gt;Haboobs&lt;/a&gt;, felt Earthquakes, seen smoke from nearby Wild Fires, and a couple of pretty bad rain storms (like the &lt;a href="https://www.desertsun.com/picture-gallery/weather/2020/02/13/2019-valentines-day-storm-and-its-aftermath-across-region/4747997002/"&gt;Valentine's Day massacre&lt;/a&gt;, and the &lt;a href="https://kesq.com/news/2014/09/11/la-quinta-cleanup-from-700-year-storm/"&gt;Storm Cell that wouldn't move&lt;/a&gt;) ... but I NEVER thought I'd experience a Tropical Storm (which was very nearly a Hurricane) but this year we did.&lt;/p&gt;
&lt;p&gt;It was a stressful day but at the end of it we can out unscathed. We were fortunate that we didn't have any property damage, but others weren't. There are still &lt;a href="https://www.desertsun.com/story/news/2023/09/03/tropical-storm-hilary-destroyed-one-palm-springs-area-neighborhood-heres-why/70733017007/"&gt;areas of the Valley that are trying to rebuild after the flooding&lt;/a&gt; that the storm brought.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;When I started writing this I didn't think i I'd have &lt;em&gt;that&lt;/em&gt; much to write, but looking back  I see that I did!&lt;/p&gt;
&lt;p&gt;I'm glad I did this and hope that future me will find some benefit from it. Hopefully 2024 me won't procrastinate writing this until the very last day ... but he probably will.&lt;/p&gt;
&lt;p&gt;That's just the nature of these things, right?&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-year-in-review-2023-1"&gt;Our current stack involves commits to Azure DevOps which is picked up by TeamCity and then deployed using  Octopus Deploy &lt;a href="#sf-year-in-review-2023-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-2"&gt;That year I ran the LA Marathon in March, and in July I tore a muscle in my left hamstring &lt;a href="#sf-year-in-review-2023-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-3"&gt;They've been in the house for almost 9 years! &lt;a href="#sf-year-in-review-2023-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-4"&gt;I read mostly Sci Fi written by people that mostly look like me &lt;a href="#sf-year-in-review-2023-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-5"&gt;These aren't particular good or well written, but I was in between books and they were on my kindle so 🤷🏼‍♂️ &lt;a href="#sf-year-in-review-2023-5-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-6"&gt;more on that in the next article 😁 &lt;a href="#sf-year-in-review-2023-6-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-7"&gt;Average costs is about $15,000 &lt;a href="#sf-year-in-review-2023-7-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-8"&gt;Close to $30,000 &lt;a href="#sf-year-in-review-2023-8-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-year-in-review-2023-9"&gt;When Emily and I were looking to buy a house we only had three requirements: (1) It couldn't be behind a gate; (2) it couldn't have a pool; (3) it had to have a citrus tree, preferably lemon. We were able to get 2 of the three when we bought the house and it only took 13 years to get the citrus tree! &lt;a href="#sf-year-in-review-2023-9-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category></entry><entry><title>So you want to give a talk at a conference?</title><link href="https://ryancheley.com/2023/12/15/so-you-want-to-give-a-talk-at-a-conference/" rel="alternate"></link><published>2023-12-15T00:00:00-08:00</published><updated>2023-12-15T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-12-15:/2023/12/15/so-you-want-to-give-a-talk-at-a-conference/</id><summary type="html">&lt;p&gt;Last October I gave my first honest to goodness, on my own, up on the stage by myself talk at a tech conference. It was the most stressful yet fulfilling professional experience I've had.&lt;/p&gt;
&lt;p&gt;Fulfilling in that I've wanted to get better at speaking in public and this helped in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last October I gave my first honest to goodness, on my own, up on the stage by myself talk at a tech conference. It was the most stressful yet fulfilling professional experience I've had.&lt;/p&gt;
&lt;p&gt;Fulfilling in that I've wanted to get better at speaking in public and this helped in that goal.&lt;/p&gt;
&lt;p&gt;Stressful in that I really wanted to do a good job and wasn't sure that I could, or worse, that anyone would care about what I had to say.&lt;/p&gt;
&lt;p&gt;Well, neither of those things turned out to be true. I did get a lot of good feedback which tells me I did a good job, and people were very encouraging for the words that I had to say, so people did care.&lt;/p&gt;
&lt;p&gt;My presentation went so well that I was even &lt;a href="https://youtu.be/WkeRI7LkBeY?si=gIgeMODD3aQJsfvX"&gt;interviewed by Jay Miller&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can see my actual talk &lt;a href="https://youtu.be/VPldDxuJDsg?si=r2ob3j4zIeYZY7tO"&gt;here&lt;/a&gt;, but I thought it would also be interesting for you to see how I got here.&lt;/p&gt;
&lt;h2&gt;Submitting the idea&lt;/h2&gt;
&lt;p&gt;I submitted my talk idea for DCUS 2023 in May and it was selected in June. That gave me roughly 3 months to get my loose outline of an idea into a 45 minute talk.&lt;/p&gt;
&lt;h2&gt;Brain storming how the talk would go&lt;/h2&gt;
&lt;p&gt;I have tried to get a better workflow for brainstorming ideas in general, but I really wanted to up my game for this talk. To that end I used the &lt;a href="https://pipdecks.com/pages/storyteller-tactics-card-deck"&gt;Story Teller Tactics&lt;/a&gt; cards to help determine the path of the story I would tell in my presentation.&lt;/p&gt;
&lt;p&gt;That helped when I got to mind mapping&lt;sup id="sf-so-you-want-to-give-a-talk-at-a-conference-1-back"&gt;&lt;a href="#sf-so-you-want-to-give-a-talk-at-a-conference-1" class="simple-footnote" title="I use MindNode for mind mapping, mostly on my iPad"&gt;1&lt;/a&gt;&lt;/sup&gt; my talk.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Mindmap of my presentation for DjangoCon US 2023" src="https://ryancheley.com/images/dcus_2023_mindmap.png"&gt;&lt;/p&gt;
&lt;p&gt;The use of the Story Teller Tactics, combined with my mind map, lead to a starting point for creating my presentation&lt;/p&gt;
&lt;h2&gt;My 'Oh Sh%t moment'&lt;/h2&gt;
&lt;p&gt;Back in early July I was browsing Mastodon (instead of working on my presentation) and came across a link to an article with the title &lt;a href="https://www.smashingmagazine.com/2023/07/become-better-speaker-conferences/"&gt;How To Become A Better Speaker At Conferences&lt;/a&gt;. I saved it to my read it later service and went on browsing. A few weeks later I actually read the article (around July 25).&lt;/p&gt;
&lt;p&gt;This bit of advice got me a little worried:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On a practical level, a 45-minute talk can take a &lt;strong&gt;surprisingly long time to put together&lt;/strong&gt;. I reckon it takes me at least an hour of preparation for every minute of content.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yikes! That means I would need to prepare and rehearse and prepare for 45 hours over the course of 14 weeks (almost 4 hours per week on average). So I set up a schedule for how I would meet this requirement&lt;/p&gt;
&lt;h2&gt;Working on the presentation&lt;/h2&gt;
&lt;p&gt;I spent all of August and the early part of September working on my presentation for about 3 hours a week. Adding slight tweaks to it here and there. I conducted a dry run with my team at work to assess my presentation's progress.&lt;/p&gt;
&lt;p&gt;The dry run went &lt;em&gt;fine&lt;/em&gt;, but it was clear that my presentation was missing &lt;em&gt;something&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Asking for feedback&lt;/h3&gt;
&lt;p&gt;Django Con offers up mentors to help you work on your talk. If you're newer to giving presentations, I &lt;strong&gt;highly&lt;/strong&gt; recommend engaging with one of them. The feedback they provide is priceless!&lt;/p&gt;
&lt;p&gt;I had the great fortune to reach out to &lt;a href="https://cloudisland.nz/@glasnt"&gt;Katie McLaughlin&lt;/a&gt; who had just given a talk at PyCon Australia titled &lt;a href="https://www.youtube.com/watch?v=YMcx35RGzYM"&gt;Present Like a Pro&lt;/a&gt;. I watched that before reaching out to her and she gave some very good advice on presentation.&lt;/p&gt;
&lt;h3&gt;Getting serious about preparing for the talk&lt;/h3&gt;
&lt;p&gt;As I said, I had done a dry run with my team at work and it was &lt;em&gt;fine&lt;/em&gt; but I could tell that the way I was working on my presentation wasn't getting it to where I wanted it to be. So I decided to go all in on practicing and trying to make it the best I could. In order to accomplish that I believe I would need to engage in &lt;strong&gt;deliberate&lt;/strong&gt; practice by actually giving my presentation. This was a breakthrough moment for me in improving the presentation and my deliver of it.&lt;/p&gt;
&lt;p&gt;In order to have deliberate practice I set up the following routine:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Give my presentation and RECORD it&lt;/li&gt;
&lt;li&gt;Watch my presentation and make notes about what needed to be improved&lt;/li&gt;
&lt;li&gt;Update my presentation based on the notes from #2&lt;/li&gt;
&lt;li&gt;Go back to step 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps 1 - 3 were done on different days. For example, on Monday I would record me giving the presentation; on Tuesday I would watch the presentation and make notes; on Wednesday I would update the presentation based on my notes from Tuesday; on Thursday I would start over.&lt;/p&gt;
&lt;p&gt;I did this a total of &lt;strong&gt;7&lt;/strong&gt; times over 21 days. Two of these times when giving the presentation I gave it to a 'live' audience and was able to get feedback from them on various parts of the presentation.&lt;/p&gt;
&lt;p&gt;I did one final dry run on October 13th (the Friday before my presentation was to happen for &lt;em&gt;real&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;That Friday was the &lt;em&gt;last&lt;/em&gt; time I even looked at my presentation before giving it. I know some people will talk about making updates on the plane ride to the conference, or the night before, or the hour before, but that would stress the crap out of me, and I was already stressed out enough!&lt;/p&gt;
&lt;h2&gt;Giving my talk for Real&lt;/h2&gt;
&lt;p&gt;On Monday October 16th I gave my talk in front of people, in real life, for the first time. Here I am up on stage with the crowd in the background&lt;/p&gt;
&lt;p&gt;&lt;img alt="Selfie of Ryan Cheley with DCUS 2023 attendees in the background" src="https://ryancheley.com/images/DCUS2023-Crowd-Selfie.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;All in all it was a really fulfilling experience, but it was pretty hard too. This was the &lt;em&gt;first&lt;/em&gt; time I spoke at a Tech Conference and I really wanted to do well.&lt;/p&gt;
&lt;p&gt;As I said before, I received some really good feedback on the talk and I was really glad to have done it.&lt;/p&gt;
&lt;p&gt;Now, you might ask, "Would I have to go to all of this trouble to prepare for a talk?"&lt;/p&gt;
&lt;p&gt;Maybe, maybe not. I just happened to find this particular prep process worked well for my brain.&lt;/p&gt;
&lt;p&gt;It was nice to hear from some of the attendees surprised that my talk was the first honest to goodness talk on my own I had ever given because it sounded so polished and well done.&lt;/p&gt;
&lt;p&gt;Practice makes better, and in this case (based on the videos) it sure did for me&lt;/p&gt;
&lt;h2&gt;A Big Thank you&lt;/h2&gt;
&lt;p&gt;A presentation like this took a lot out of me, but I am extremely grateful to a few people in particular:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudisland.nz/@glasnt"&gt;Katie McLaughlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The Team of Web Developers at work&lt;/li&gt;
&lt;li&gt;Bookie&lt;/li&gt;
&lt;li&gt;Chris&lt;/li&gt;
&lt;li&gt;Jason&lt;/li&gt;
&lt;li&gt;Jon&lt;/li&gt;
&lt;li&gt;My daughter Abigail&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A little bit extra&lt;/h2&gt;
&lt;p&gt;If you want to see more details on my talk, here is a &lt;a href="https://www.youtube.com/playlist?list=PLMHsf-A9W6iXadPsvD-7Efqo860LpFpxP"&gt;playlist of the dry run attempts&lt;/a&gt; I did to prepare&lt;sup id="sf-so-you-want-to-give-a-talk-at-a-conference-2-back"&gt;&lt;a href="#sf-so-you-want-to-give-a-talk-at-a-conference-2" class="simple-footnote" title="Be careful, there is at least one not-safe-for-work word in one of the early videos."&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;If you want to see the repo where the changes were tracked for my presentation it can be found &lt;a href="https://github.com/ryancheley/djangocon-us-2023"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you want to see my annotated slides, you can find them &lt;a href="https://annotated-notes.ryancheley.com/dcus2023/annotated-slides.html"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Time tracking&lt;/h2&gt;
&lt;p&gt;I time track the crap out of my work day, and I &lt;em&gt;really&lt;/em&gt; wish I would have done that here just to get a more exact idea of how much time I spent preparing, but some basic back of the envelope math gives me nearly 56 hours of prep for this. One thing I do religiously for work is track my time. I do this for a couple of reasons, but for some reason, I didn't do that as I prepared for my talk here. The time I have here is mostly estimates based on my memory (which could be wildly over stated, or understated).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Time Spent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Story Teller Tactics Work&lt;/td&gt;
&lt;td&gt;1.5 Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mind Mapping Talk&lt;/td&gt;
&lt;td&gt;3 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial Draft of Presentation&lt;/td&gt;
&lt;td&gt;5 Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presentation Updates&lt;/td&gt;
&lt;td&gt;18 Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliberate Practice&lt;/td&gt;
&lt;td&gt;28 Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.5 hours&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-so-you-want-to-give-a-talk-at-a-conference-1"&gt;I use MindNode for mind mapping, mostly on my iPad &lt;a href="#sf-so-you-want-to-give-a-talk-at-a-conference-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-so-you-want-to-give-a-talk-at-a-conference-2"&gt;Be careful, there is at least one not-safe-for-work word in one of the early videos. &lt;a href="#sf-so-you-want-to-give-a-talk-at-a-conference-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="dcus"></category><category term="conference-talks"></category></entry><entry><title>Error Culture Part III</title><link href="https://ryancheley.com/2023/11/14/error-culture-part-iii/" rel="alternate"></link><published>2023-11-14T00:00:00-08:00</published><updated>2023-11-14T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-11-14:/2023/11/14/error-culture-part-iii/</id><summary type="html">&lt;h1&gt;How can I tell if I'm in an error culture?&lt;/h1&gt;
&lt;p&gt;In part 1 I spoke about the idea of &lt;a href="https://www.ryancheley.com/2023/10/29/error-culture/"&gt;Error Culture&lt;/a&gt;. In that post I define what error culture.&lt;/p&gt;
&lt;p&gt;In part 2 I spoke when &lt;a href="https://www.ryancheley.com/2023/11/09/error-culture-part-ii/"&gt;Error Culture&lt;/a&gt; starts. This time I'll talk about how you can tell if you're living …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;How can I tell if I'm in an error culture?&lt;/h1&gt;
&lt;p&gt;In part 1 I spoke about the idea of &lt;a href="https://www.ryancheley.com/2023/10/29/error-culture/"&gt;Error Culture&lt;/a&gt;. In that post I define what error culture.&lt;/p&gt;
&lt;p&gt;In part 2 I spoke when &lt;a href="https://www.ryancheley.com/2023/11/09/error-culture-part-ii/"&gt;Error Culture&lt;/a&gt; starts. This time I'll talk about how you can tell if you're living in an Error Culture, and what you can do about it.&lt;/p&gt;
&lt;p&gt;Below are a couple of tell-tale signs I've found to determine if you're living in an error culture.&lt;/p&gt;
&lt;h2&gt;Email Rules&lt;/h2&gt;
&lt;p&gt;You start your day and fire up your email client. As the application opens up you see the number of unread message go from 500 down to 20. You think back to a time when you would open your email client and have to trod through ALL 500 of those emails. Now though ... now you've outsmarted the email system by implementing several rules to ignore or hide those pesky emails that don't seem to mean anything.&lt;/p&gt;
&lt;h2&gt;Instinct to just delete emails&lt;/h2&gt;
&lt;p&gt;Maybe you don't know about the amazing opportunities that email client rules offer, so you start going through your emails. You delete the ones you &lt;strong&gt;know&lt;/strong&gt; aren't useful or don't mean anything.&lt;/p&gt;
&lt;p&gt;Or maybe you do know about rules and of the remaining 20 you notice a few new emails that you don't need to act on. Your first instinct is to delete them, but you remember you are a smart email user and create a new rule to get rid of those emails as well.&lt;/p&gt;
&lt;h2&gt;Why do I get this email anyway?&lt;/h2&gt;
&lt;p&gt;If you use rules, you recall a time before you had them. A time when you would methodically read each email and write down a quick note to ask a co-worker, or your boss at your next one on one. But when you brought up the alerts you had one of two reactions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Oh those ... yeah, you can just delete them. They don't mean anything&lt;/li&gt;
&lt;li&gt;Ugh ... how do you &lt;strong&gt;not&lt;/strong&gt; know what that is for? Fine, let me explain it to you ... &lt;strong&gt;again&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first item is definitely error culture. The second response could be error culture if the person you've asked is just so overwhelmed with all of the alerts ... OR it could just be a toxic culture. If it's a toxic culture, I'm sorry, but this post might not be helpful in solving that problem.&lt;/p&gt;
&lt;p&gt;If you're not in the second situation you may (rightfully) ask&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;why do we get it if we can just delete it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And if the answer is 🤷‍♂️ then you might be in an error culture.&lt;/p&gt;
&lt;p&gt;In general, if no one knows WHY we're getting an email and there is no actionable direction, you might be in an error culture.&lt;/p&gt;
&lt;h2&gt;Email Alerts&lt;/h2&gt;
&lt;p&gt;Ask yourself, your peers, and your boss this question&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is this alert we are getting actually important?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the answer is No, then delete the mechanism that generates the error. Don't just create a rule to delete the alert.&lt;/p&gt;
&lt;p&gt;If the answer is Yes, then ask&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is the alert you are getting actionable?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the answer is No then update the alert to be actionable. This can be done by&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Including steps to resolution or documentation link for resolving the error&lt;/li&gt;
&lt;li&gt;Update the alert to indicate it’s importance&lt;/li&gt;
&lt;li&gt;Update the alert to go to the correct people&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer is Yes then&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make sure the error indicates what the fix needs to be&lt;/li&gt;
&lt;li&gt;Make sure the error indicates why it’s important, or a link to documentation that explains it&lt;/li&gt;
&lt;li&gt;Make sure the right people are being notified&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Point three here is really important. To determine if the correct people are being notified ask this questions of EVERYONE that receives the alert:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Are you the correct person to do something to fix the error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the answer is No then getting removed from the email is the best course of action.&lt;/p&gt;
&lt;p&gt;Of course, it could be that no one ever told you why you were getting the alert so the decision to remove people from alerts may need to be a management level decision, but it can at least start the conversation.&lt;/p&gt;
&lt;p&gt;If the answer is Yes then do you (i.e. the person being asked) know what to do to fix the error&lt;/p&gt;
&lt;p&gt;Again, with a simple yes or no response, you have two options:&lt;/p&gt;
&lt;p&gt;Yes: Does the error indicate what the fix needs to be or where to go to find out?
No: Work to update the error to make it actionable&lt;/p&gt;
&lt;p&gt;This can help to get the right people getting the alerts.&lt;/p&gt;
&lt;p&gt;Below is a flow chart to help make alerts better&lt;/p&gt;
&lt;p&gt;&lt;img alt="Diagram of how to make alerts better" src="https://ryancheley.com/images/alert_flow_diagram.png"&gt;&lt;/p&gt;
&lt;p&gt;None of this is easy to change. You may have managers that don't answer your questions when asking about if someone should receive an alert.&lt;/p&gt;
&lt;p&gt;You may not get feedback from your peers, or manager about cleaning up the alert system. But if you can become a champion for the effort it will be very helpful for everyone involved.&lt;/p&gt;
&lt;p&gt;If you implement something like this you can increase the signal to noise ratio for you and your team. That seems like a big win for everyone.&lt;/p&gt;</content><category term="musings"></category><category term="culture"></category><category term="programming"></category></entry><entry><title>Error Culture Part II</title><link href="https://ryancheley.com/2023/11/09/error-culture-part-ii/" rel="alternate"></link><published>2023-11-09T00:00:00-08:00</published><updated>2023-11-09T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-11-09:/2023/11/09/error-culture-part-ii/</id><summary type="html">&lt;p&gt;In my last post I spoke about the idea of &lt;a href="https://www.ryancheley.com/2023/10/29/error-culture/"&gt;Error Culture&lt;/a&gt;. In that post I define what error culture. This time I'll talk about when it starts to happen. For a recap go back and read that before diving in here.&lt;/p&gt;
&lt;h1&gt;When does error culture start?&lt;/h1&gt;
&lt;p&gt;Error culture can …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In my last post I spoke about the idea of &lt;a href="https://www.ryancheley.com/2023/10/29/error-culture/"&gt;Error Culture&lt;/a&gt;. In that post I define what error culture. This time I'll talk about when it starts to happen. For a recap go back and read that before diving in here.&lt;/p&gt;
&lt;h1&gt;When does error culture start?&lt;/h1&gt;
&lt;p&gt;Error culture can start because of internal reason, external reason, or both and are almost always driven by the best of intentions. Error culture starts to happen because we don't finish the alert process. That is, we set up the alerts, but we don't indicate why they are important or what to do about them when we're notified.&lt;/p&gt;
&lt;h2&gt;Internal&lt;/h2&gt;
&lt;p&gt;Internal pressures driving error culture can usually be traced back to someone (usually someone important &lt;sup id="sf-error-culture-part-ii-1-back"&gt;&lt;a href="#sf-error-culture-part-ii-1" class="simple-footnote" title="important here just means someone with influence"&gt;1&lt;/a&gt;&lt;/sup&gt;) declaring that ‘we’ need to be notified of when ‘this’ happens again. In and of itself self, this is actually a really good idea.&lt;/p&gt;
&lt;p&gt;But if the important person doesn't identify &lt;strong&gt;why&lt;/strong&gt; we need to be notified all that happens is that an alert is set up and NO ONE knows what to do when it fires off.&lt;/p&gt;
&lt;p&gt;The opposite side of the coin here is being proactive in wanting to be notified when a bad thing &lt;strong&gt;might&lt;/strong&gt; happen and being notified &lt;strong&gt;might&lt;/strong&gt; be useful. Again, if there is no definition for why the alert might be useful, you're simply creating noise and encouraging alerts to be ignored.&lt;/p&gt;
&lt;h2&gt;External&lt;/h2&gt;
&lt;p&gt;External pressures that can drive error culture are similar to internal ones. There are some slight differences though.&lt;/p&gt;
&lt;p&gt;For example, a consultant might indicate that it is &lt;code&gt;best practice TM&lt;/code&gt; to be notified of an alert. However, they don't provide more context for why it's best practice. It could very well be that the recommendation IS best practice, but for a user base that is 100x your user base, or for an organization that is 1/10th your size. Context matters and while best practices should scale, they don't always.&lt;/p&gt;
&lt;p&gt;Another example of external drivers are software applications provided by third party vendors with default alerts enabled but no context or steps for resolution. Sometimes there will be documentation describing the alert process, but without the context for why the alert is important it's just as likely to be ignored.&lt;/p&gt;
&lt;p&gt;So far in this series we've seen what error culture is,and when it starts to happen. In the next post I'll talk about how to identify if you're in an error culture.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-error-culture-part-ii-1"&gt;important here just means someone with influence &lt;a href="#sf-error-culture-part-ii-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="culture"></category><category term="programming"></category></entry><entry><title>Error Culture</title><link href="https://ryancheley.com/2023/10/29/error-culture/" rel="alternate"></link><published>2023-10-29T00:00:00-07:00</published><updated>2023-10-29T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-10-29:/2023/10/29/error-culture/</id><summary type="html">&lt;h2&gt;What is Error Culture?&lt;/h2&gt;
&lt;p&gt;It's inevitable that at some point a service &lt;sup id="sf-error-culture-1-back"&gt;&lt;a href="#sf-error-culture-1" class="simple-footnote" title="When I say service here I mean very loosely anything from a micro service up to a physical server."&gt;1&lt;/a&gt;&lt;/sup&gt; will fail. When that service fails you can either choose to be alerted, or not. Because technology is so important to so many aspects of work, not getting an alert for a failing service isn't really an …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;What is Error Culture?&lt;/h2&gt;
&lt;p&gt;It's inevitable that at some point a service &lt;sup id="sf-error-culture-1-back"&gt;&lt;a href="#sf-error-culture-1" class="simple-footnote" title="When I say service here I mean very loosely anything from a micro service up to a physical server."&gt;1&lt;/a&gt;&lt;/sup&gt; will fail. When that service fails you can either choose to be alerted, or not. Because technology is so important to so many aspects of work, not getting an alert for a failing service isn't really an option. So we enable alerts ... for EVERYTHING.&lt;/p&gt;
&lt;p&gt;This is good in that we know when things have gone bad ... but it's bad in that we can start to ignore these alerts because we get false positives. If you hear comments like,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Oh yeah, that error always comes up, but we just ignore it because it doesn't mean anything&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We don't really know why that error occurs, but it doesn't seem to impact anything, so we just ignore it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what I am calling, "Error Culture".&lt;/p&gt;
&lt;h2&gt;OK, but why is that bad?&lt;/h2&gt;
&lt;p&gt;Initially, it might not &lt;em&gt;feel&lt;/em&gt; bad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EVERYONE&lt;/strong&gt; knows that you can ignore that error because it doesn't mean anything. Of course, this knowledge tends to &lt;strong&gt;NOT&lt;/strong&gt; be documented anywhere, so when you onboard new team members they don't know what &lt;strong&gt;EVERYONE&lt;/strong&gt; knows ... because they weren't part of the &lt;strong&gt;EVERYONE&lt;/strong&gt; that learned the lesson.&lt;/p&gt;
&lt;p&gt;Additionally, if you're getting error messages and nothing truly bad every happens, then a few things can happen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;People start to question ALL of the alerts. I mean, if this one isn't valid, why is this OTHER one valid? Maybe I can ignore both 🤷‍♂️&lt;/li&gt;
&lt;li&gt;You may be getting an alert about a small thing that can be ignored until it's a BIG thing. I think this image does good job of illustrating the point (found &lt;a href="https://naksecurity.medium.com/the-detriments-of-hero-culture-3fc455963d6e"&gt;here&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="We have a Problem Here!" src="https://miro.medium.com/v2/resize:fit:854/format:webp/1*QQvTuD-5AH2NKdh1_B_teQ.jpeg"&gt;&lt;/p&gt;
&lt;h2&gt;Why does it happen?&lt;/h2&gt;
&lt;p&gt;In general, I've found that error culture can happen for a few reason&lt;/p&gt;
&lt;h3&gt;Error Fatigue&lt;/h3&gt;
&lt;p&gt;If you get 1000 alerts every day, you're not going to be able to do anything about anything. This is similar phenomenon to 'Alert Fatgiue' which can happen in software applications (my experience is in Electronic Health Record systems) where users can just click &lt;code&gt;OK&lt;/code&gt; or &lt;code&gt;Cancel&lt;/code&gt; when an alert shows up and users may not actually see that there is a problem&lt;/p&gt;
&lt;h3&gt;Lack of understanding of what the error is&lt;/h3&gt;
&lt;p&gt;It's surprising to find that people that receive alerts and they just delete them. They do this not out malice, but because they honeslty do not know what the alert is for. They were maybe opted into the alert (with no way to opt out) and therefore have no idea why they get it or what they are supposed to do with it. They may also be in an organization where asking questions to learn isn't encouraged and will therefore not ask why they are getting the alert.&lt;/p&gt;
&lt;h3&gt;Lack of understanding of why the error is important&lt;/h3&gt;
&lt;p&gt;Related to the item above, but different, a person can receive an alert, but they don't understand why it's important. This is usually manifested in some of the things mentioned before. Ideas like,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;well, I've ignored this alert every day for 6 months, I don't know why I need to do anything about it now!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Lack of understand of who the error will impact&lt;/h3&gt;
&lt;p&gt;I'm reminded of the Episode of &lt;a href="https://youtu.be/pMuVm1Y669U?si=--E-MDfTWPlHjBqk&amp;amp;t=180"&gt;Friends&lt;/a&gt; where there is a light switch in Chandler and Joey's apartment and they don't know what it's for. At the end of the episide Monica is idly flipping the switch off and on and the camera pans to a Monica and Rachel's apartment where their TV keeps turning off and on.&lt;/p&gt;
&lt;p&gt;Error culture can have a similar feeling. If I get an error every few days, but it doesn't impact me or my work I am likely to ignore it. It could be that the error is unimporatnt for me, but HUGELY important for you. This is a case where the error is being directled incorrectly. If we both got the error you could see that I got the email and then ask, hey, are you going to do anything about this?&lt;/p&gt;
&lt;h3&gt;Emphasis on Hero Culture&lt;/h3&gt;
&lt;p&gt;This is probably the worst of all possibilities. Some cultures tend to emphasize Heroes or White Knights. They appreciate when someone comes in and 'Saves the Day'. Sometimes people get promoted because of this.&lt;/p&gt;
&lt;p&gt;This tends to disincentivize the idea of fixing small problems before they become BIG problems. I might be getting an alert about an issue, but it's not a BIG deal and won't be for some time. Once it becomes a big deal I'll know how to fix it quickly, and I will. When I do, I'll be celebrated. Who wouldn't want that?&lt;/p&gt;
&lt;p&gt;In this post I've identified some of the characteristics of Error Culture.&lt;/p&gt;
&lt;p&gt;In the next post I'll talk about how to tell if you're in an Error Culture.&lt;/p&gt;
&lt;p&gt;In the final post I'll write about what you might be able to do to mitigate, and maybe even eliminate, Error Culture where you are.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-error-culture-1"&gt;When I say service here I mean very loosely anything from a micro service up to a physical server. &lt;a href="#sf-error-culture-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="culture"></category><category term="programming"></category></entry><entry><title>DjangoCon US 2023</title><link href="https://ryancheley.com/2023/10/24/djangocon-us-2023/" rel="alternate"></link><published>2023-10-24T00:00:00-07:00</published><updated>2023-10-24T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-10-24:/2023/10/24/djangocon-us-2023/</id><summary type="html">&lt;h1&gt;My Experience at DjangoCon US 2023&lt;/h1&gt;
&lt;p&gt;A few days ago I returned from DjangoCon US 2023 and wow, what an amazing time. The only regret I have is that I didn't take very many pictures. This is something I will need to work on for next year.&lt;/p&gt;
&lt;p&gt;On Monday October …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;My Experience at DjangoCon US 2023&lt;/h1&gt;
&lt;p&gt;A few days ago I returned from DjangoCon US 2023 and wow, what an amazing time. The only regret I have is that I didn't take very many pictures. This is something I will need to work on for next year.&lt;/p&gt;
&lt;p&gt;On Monday October 16th I gave a talk &lt;a href="https://2023.djangocon.us/talks/contributing-to-django-or-how-i-learned-to-stop-worrying-and-just-try-to-fix-an-orm-bug/"&gt;Contributing to Django or how I learned to stop worrying and just try to fix an ORM Bug&lt;/a&gt;. The video will be posted on YouTube in a few weeks. This was the first tech conference I've ever spoken at!!!! I was super nervous leading up to the talk, and even a bit at the start, but once I got going I finally settled in.&lt;/p&gt;
&lt;p&gt;Here's me on stage taking a selfie with the crowd behind me&lt;/p&gt;
&lt;p&gt;&lt;img alt="Selfie of Ryan Cheley with DCUS 2023 attendees in the background" src="https://ryancheley.com/images/DCUS2023-Crowd-Selfie.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;Luckily, my talk was one of the first non-Keynote talks so I was able to relax and enjoy the conference while the rest of the time.&lt;/p&gt;
&lt;p&gt;After the conference talks ended on Wednesday I stuck around for the sprints. This is such a great time to be able to work on open source projects (Django adjacent or not) and just generally hang out with other Djangonauts. I was able to do some work on DjangoPackages with Jeff Triplett, and just generally hang out with some truly amazing people.&lt;/p&gt;
&lt;p&gt;The Django community is just so great. I've been to many conferences before, but this one is the first where I feel like I belong.&lt;/p&gt;
&lt;p&gt;I am having some of those post conference blues, but thankfully Kojo Idrissa wrote something about how to &lt;a href="https://kojoidrissa.com/conferences/community/pycon%20africa/noramgt/2019/08/11/post_conference_depression.html"&gt;help with that&lt;/a&gt;. And taking his advice, it has been helpful to come down from the Conference high.&lt;/p&gt;
&lt;p&gt;Although the location of DjangoCon US 2024 hasn't been announced yet, I'm making plans to attend.&lt;/p&gt;
&lt;p&gt;I am also setting myself some goals to have completed by the start of DCUS 2024&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;join the fundraising working group&lt;/li&gt;
&lt;li&gt;work on at least 1 code related ticket in Trac&lt;/li&gt;
&lt;li&gt;work on at least 1 doc related ticket in Trac&lt;/li&gt;
&lt;li&gt;have been part of a writing group with fellow Djangonauts and posted at least 1 article per month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I had a great experience speaking, and I &lt;strong&gt;think&lt;/strong&gt; I'd like to do it again, but I'm still working through that.&lt;/p&gt;
&lt;p&gt;It's a lot harder to give a talk than I thought it would be! That being said, I do have in my 'To Do' app a task to 'Brainstorm DjangoCon talk ideas' so we'll see if (1) I'm able to come up with anything, and (2) I have a talk accepted for 2024.&lt;/p&gt;</content><category term="technology"></category><category term="django"></category></entry><entry><title>Firebirds Inaugural Season</title><link href="https://ryancheley.com/2023/07/01/firebirds-inaugural-season/" rel="alternate"></link><published>2023-07-01T00:00:00-07:00</published><updated>2023-07-01T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-07-01:/2023/07/01/firebirds-inaugural-season/</id><summary type="html">&lt;p&gt;On Wednesday June 21, 2023 the local sports puck team (i.e. Hockey), the &lt;a href="https://cvfirebirds.com/"&gt;Coachella Valley Firebirds&lt;/a&gt; hosted &lt;a href="https://theahl.com/stats/game-center/1025179"&gt;Game 7&lt;/a&gt; of the &lt;a href="https://en.wikipedia.org/wiki/Calder_Cup"&gt;Calder Cup&lt;/a&gt; Finals against the &lt;a href="https://www.hersheybears.com/"&gt;Hershey Bears&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are sports writers that can write on how the series went, better than I can so I'll leave that to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;On Wednesday June 21, 2023 the local sports puck team (i.e. Hockey), the &lt;a href="https://cvfirebirds.com/"&gt;Coachella Valley Firebirds&lt;/a&gt; hosted &lt;a href="https://theahl.com/stats/game-center/1025179"&gt;Game 7&lt;/a&gt; of the &lt;a href="https://en.wikipedia.org/wiki/Calder_Cup"&gt;Calder Cup&lt;/a&gt; Finals against the &lt;a href="https://www.hersheybears.com/"&gt;Hershey Bears&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are sports writers that can write on how the series went, better than I can so I'll leave that to the pros. What I will talk about is why watching that game and seeing the Firebirds lose in Overtime hit me so hard.&lt;/p&gt;
&lt;p&gt;I'm generally an introverted person. Even before the pandemic, I wasn't particularly fond of attending crowded events. The pandemic only intensified my preference for solitude. Suddenly, I found myself being advised to avoid social interactions altogether. As an introvert, the circumstances necessitating isolation weren't exactly ideal for me, but I did appreciate the fact that my family and I had to isolate.&lt;/p&gt;
&lt;p&gt;However, after 2+ years of isolating from most everyone, being in large groups would bring out anxiety. And when I say large groups I mean like 10, maybe 15 people. On December 18th there was work holiday get together, the first one since the pandemic started. There were about 100 people in a mostly enclosed space and I did not do well with it. Super anxious, wore a mask the entire time, and generally ducked into the closet that also serves as my office more than once just to get away from people.&lt;/p&gt;
&lt;p&gt;That same night was the home opener for the Firebirds at Acrisure Arena (due to construction delays their home arena opened 2 1/2 months after the start of the season). I didn't know it at the time, but it was a sell out (attendance of 10,087). This meant that I was going to a sporting event, in an enclosed arena with 10,000+ people. To say that I nearly lost my shit would be an understatement. The only thing that really got me to go was that the tickets I had purchased weren't cheap, and my wife and I were going with another couple friend.&lt;/p&gt;
&lt;p&gt;That &lt;a href="https://theahl.com/stats/game-center/1024284"&gt;first home game&lt;/a&gt; was amazing. The Firebirds won 4-3 over the Tucson Roadrunners. The energy was amazing and I decided that I &lt;em&gt;had&lt;/em&gt; to go to another game. And so I kept going. Again and again and again. I saw 34 games in person with an average attendance of 7,500.&lt;/p&gt;
&lt;p&gt;I'd like to say that "just like that" my anxiety surrounding large indoor gatherings was gone, but it wasn't. It took me going to lots of hockey games to get through it.&lt;/p&gt;
&lt;p&gt;So coming back to game 7 on Wednesday night. With less than 1 minute into the second period the Firebirds scored their second goal to go up 2-0. The crowd was the loudest I'd ever heard at Acrisure. Chants of "we want the cup" roared through the arena. It was unreal. And I sat there and realized that if it hadn't been for this team my anxiety surrounding large gatherings wouldn't have gone away for probably a very long time. And other than being a HUGE fan, I wanted the players, coaches, and team to win because they had helped me deal with something so personal. I won't ever be able to repay them for that, but my cheering them on to try and win the cup could maybe start.&lt;/p&gt;
&lt;p&gt;And then the unthinkable happened. A penalty was called on the Firebirds and a Power Play goal was scored. Then less than 4 minutes later an even strength goal was scored and we were tied at 2 a piece.&lt;/p&gt;
&lt;p&gt;The third period ended without any scorning by either team, and for only the second time in Calder Cup finals history, the first time since 1953, we were going to Overtime in a Game 7.&lt;/p&gt;
&lt;p&gt;As we entered Overtime everyone in my section (107) was on their feet. We stood for the entire overtime period. Cheering, and screaming (honestly, I was still exhausted from the experience as I wrote this 2 days later).&lt;/p&gt;
&lt;p&gt;About 2 minutes into the Overtime period Ryker Evan sent a shot on goal. From where I was sitting I could see the flight of the puck and my heart leapt as I thought it would find the back of the net ... but sadly it didn't. Within the first five minutes of overtime the Firebirds had outshot the Bears 5-0. It seemed like we were in control.&lt;/p&gt;
&lt;p&gt;The next 10 minutes was some of the most intense back and forth hockey I'd ever seen.&lt;/p&gt;
&lt;p&gt;With less than 4 minutes on the clock I thought, this might go into double overtime ... and then the unthinkable happened. The Firebirds defense was unable to clear a puck in their end, lots of players in front of the net, and just like that I see a puck flying over Joey's shoulder and past the cross bar, hitting the back of the net. The Bears player and their fans roared with joy, and suddenly a once deafening Acrisure was stunned into silence.&lt;/p&gt;
&lt;p&gt;We lost. They won. The inaugural season was over. I stood in disbelief for a minute and then just sat down and stared across the arena at the Bears fans I could see that were losing their minds with joy. I wanted to cry. Some people around me did.&lt;/p&gt;
&lt;p&gt;I stood up and looked over at our defensive end. The Firebirds players on the ice had taken a knee as they watched the Bears players celebrate. They don't show that part on TV. The defeated team looking sadly on as the victors celebrate. It was heartbreaking.&lt;/p&gt;
&lt;p&gt;And then, in the middle of the celebration, the chants of "Let's go Firebirds" started. In short order, the fans were all saying it as loud as they could. An amazing season that didn't end the way we wanted it to, but we did our best to let the team know what they meant to us.&lt;/p&gt;
&lt;p&gt;When I started writing this I thought maybe it was just me that needed something like this to get over some of the anxiety of large indoor gatherings, but maybe it was others. And those others at that game let the team know how much we appreciated them and what they did. This team will always hold a special place in the hearts of it's fans.&lt;/p&gt;
&lt;p&gt;We didn't win it all this year, but there's always next year. Always.&lt;/p&gt;
&lt;h2&gt;Postlude&lt;/h2&gt;
&lt;p&gt;A friend of a friend of a friend works at a golf course called the 'Classic Club'. There were 3 players that were golfing the next day and they told this friend of a friend of a friend that the chants of "Let's go Firebirds" even after the loss meant so much to them.&lt;/p&gt;</content><category term="musings"></category><category term="hockey"></category><category term="ahl"></category></entry><entry><title>GCP Cloud Architect Exam Experience</title><link href="https://ryancheley.com/2023/04/01/gcp-cloud-architect-exam-experience/" rel="alternate"></link><published>2023-04-01T00:00:00-07:00</published><updated>2023-04-01T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2023-04-01:/2023/04/01/gcp-cloud-architect-exam-experience/</id><summary type="html">&lt;p&gt;&lt;a href="https://www.fiercehealthcare.com/health-tech/google-health-notches-another-provider-partner-care-studio"&gt;Last October it was announced&lt;/a&gt; that Desert Oasis Healthcare (the company I work for) signed on to pilot &lt;a href="https://health.google/caregivers/care-studio/"&gt;Google's Care Studio&lt;/a&gt;. DOHC is the first ambulatory clinic to sign on.&lt;/p&gt;
&lt;p&gt;I had been in some of the discovery meetings before the announcement and was really excited about the opportunity. So …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://www.fiercehealthcare.com/health-tech/google-health-notches-another-provider-partner-care-studio"&gt;Last October it was announced&lt;/a&gt; that Desert Oasis Healthcare (the company I work for) signed on to pilot &lt;a href="https://health.google/caregivers/care-studio/"&gt;Google's Care Studio&lt;/a&gt;. DOHC is the first ambulatory clinic to sign on.&lt;/p&gt;
&lt;p&gt;I had been in some of the discovery meetings before the announcement and was really excited about the opportunity. So far our use of any Cloud platforms at work has been extremely limited (that is to say, we don't use ANY of the big three cloud solutions for our tech) so this seemed to provide a really good opportunity.&lt;/p&gt;
&lt;p&gt;As we worked through the project scoping there were conversations about the handoff to DOHC and it occurred to me that I didn't have any knowledge of what GCP offered, what any of it did, or how any of it could work.&lt;/p&gt;
&lt;p&gt;I've had on my 'To Do' list to learn one of the Big Three Cloud services (AWS, Azure, or GCP) but because we didn't use ANY of them at work I was (a) worried about picking the 'wrong' one and (b) worried that even if I picked one I'd NEVER be able to use it!&lt;/p&gt;
&lt;p&gt;The partnership with Google changed that. Suddenly which cloud service to learn was apparent AND I'd be able to use whatever I learned for work!&lt;/p&gt;
&lt;p&gt;Great, now I know which cloud service to start to learn about ... the next question is, "What do I try to learn?". In speaking with some of the folks at Google they recommended one of three Certification options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/certification/cloud-digital-leader"&gt;Digital Cloud Leader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/certification/cloud-engineer"&gt;Cloud Engineer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/certification/cloud-architect"&gt;Cloud Architect&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After reviewing each of them and having a good idea of what I &lt;strong&gt;need&lt;/strong&gt; to know for work, I opted for the Cloud Architect path.&lt;/p&gt;
&lt;p&gt;Knowing which certification I was going to work towards, I started to see what learning options were available for me. It just so happens that &lt;a href="https://blog.coursera.org/coursera-partners-with-the-california-state-library-to-launch-free-statewide-job-training-program/"&gt;Coursera partnered with the California State Library to offer free training&lt;/a&gt; which is great because Coursera has &lt;a href="https://www.coursera.org/professional-certificates/gcp-cloud-architect"&gt;a learning path for the Cloud Architect Exam&lt;/a&gt;! So I signed up for the first course of that path right before Thanksgiving and started to work my way through the courses.&lt;/p&gt;
&lt;p&gt;I spent most of the holidays working through these courses, going pretty fast through them. The labs offered up are so helpful. They actually allow you to work with GCP for FREE during your labs which is amazing.&lt;/p&gt;
&lt;p&gt;After I made my way through the Coursera learning Path I bought the book &lt;a href="https://www.amazon.com/dp/1119871050?psc=1&amp;amp;ref=ppx_yo2ov_dt_b_product_details"&gt;Google Cloud Certified Professional Cloud Architect Study Guide&lt;/a&gt; which was really helpful. It came with 100 electronic flash cards and 2 practice exams, and each chapter had questions at the end.&lt;/p&gt;
&lt;p&gt;I will say that the practice exams and chapter questions from the book weren't really like the ACTUAL exam questions BUT it did help me in my learning, especially regarding the case studies used in the exams.&lt;/p&gt;
&lt;p&gt;I read through the book several times, and used the practice questions in the chapters to drive what parts of the documentation I'd read to shore up my understand of the topics.&lt;/p&gt;
&lt;p&gt;Finally, after about 3 months of pretty constant studying I took the test. I opted for the remote proctoring option and I'd say that I really liked this option. I was able to take the test in the same place I had done most of my studying. I did have to remove essentially EVERYTHING from my home office, but not having to drive anywhere, and not having to worry about unfamiliar surroundings really helped me out (I think).&lt;/p&gt;
&lt;p&gt;I had 2 hours in which to answer 60 questions. My general strategy for taking tests is to go through the test, mark questions that I'm unsure of and eliminate answers that I know to not be true on those questions. Once I've gone through the test I revisit all of the unsure questions and work through those.&lt;/p&gt;
&lt;p&gt;My final pass is to go through ALL of the questions and make sure I didn't do something silly.&lt;/p&gt;
&lt;p&gt;Using this strategy I used 1 hour and 50 minutes of the 2 hours ... and I passed!&lt;/p&gt;
&lt;p&gt;The unfortunate part of the test is that you only get a Pass or Fail so you don't have any opportunity to know what parts of the exam you missed. Now, if you fail this could be a huge help in working to pass it next time, but even if you pass it I think it would be helpful to know what areas you might struggle in.&lt;/p&gt;
&lt;p&gt;All in all this was a pretty great experience and it's already helping with the GCP implementation at work. I'm able to ask better questions because I'm at least aware of the various services and what they do.&lt;/p&gt;</content><category term="technology"></category><category term="gcp"></category></entry><entry><title>Contributing to Django or how I learned to stop worrying and just try to fix an ORM Bug</title><link href="https://ryancheley.com/2022/11/12/contributing-to-django/" rel="alternate"></link><published>2022-11-12T00:00:00-08:00</published><updated>2022-11-12T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-11-12:/2022/11/12/contributing-to-django/</id><summary type="html">&lt;p&gt;I went to &lt;a href="https://2022.djangocon.us"&gt;DjangoCon US&lt;/a&gt; a few weeks ago and &lt;a href="https://twitter.com/pauloxnet/status/1583350887375773696"&gt;hung around for the sprints&lt;/a&gt;. I was particularly interested in working on open tickets related to the ORM. It so happened that &lt;a href="https://github.com/charettes"&gt;Simon Charette&lt;/a&gt; was at Django Con and was able to meet with several of us to talk through …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I went to &lt;a href="https://2022.djangocon.us"&gt;DjangoCon US&lt;/a&gt; a few weeks ago and &lt;a href="https://twitter.com/pauloxnet/status/1583350887375773696"&gt;hung around for the sprints&lt;/a&gt;. I was particularly interested in working on open tickets related to the ORM. It so happened that &lt;a href="https://github.com/charettes"&gt;Simon Charette&lt;/a&gt; was at Django Con and was able to meet with several of us to talk through the inner working of the ORM.&lt;/p&gt;
&lt;p&gt;With Simon helping to guide us, I took a stab at an open ticket and settled on &lt;a href="https://code.djangoproject.com/ticket/10070"&gt;10070&lt;/a&gt;. After reviewing it on my own, and then with Simon, it looked like it wasn't really a bug anymore, and so we agreed that I could mark it as &lt;a href="https://code.djangoproject.com/ticket/10070#comment:22"&gt;done&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Kind of anticlimactic given what I was &lt;strong&gt;hoping&lt;/strong&gt; to achieve, but a closed ticket is a closed ticket! And so I &lt;a href="https://twitter.com/ryancheley/status/1583206004744867841"&gt;tweeted out my accomplishment&lt;/a&gt; for all the world to see.&lt;/p&gt;
&lt;p&gt;A few weeks later though, a &lt;a href="https://code.djangoproject.com/ticket/10070#comment:22"&gt;comment&lt;/a&gt; was added that it actually was still a bug and it was reopened.&lt;/p&gt;
&lt;p&gt;I was disappointed ... but I now had a chance to actually fix a real bug! &lt;a href="https://github.com/ryancheley/public-notes/issues/1#issue-1428819941"&gt;I started in earnest&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A suggestion / pattern for working through learning new things that &lt;a href="https://simonwillison.net"&gt;Simon Willison&lt;/a&gt; had mentioned was having a &lt;code&gt;public-notes&lt;/code&gt; repo on GitHub. He's had some great stuff that he's worked through that you can see &lt;a href="https://github.com/simonw/public-notes/issues?q=is%3Aissue"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using this as a starting point, I decided to &lt;a href="https://github.com/ryancheley/public-notes/issues/1"&gt;walk through what I learned while working on this open ticket&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Over the course of 10 days I had a 38 comment 'conversation with myself' and it was &lt;strong&gt;super&lt;/strong&gt; helpful!&lt;/p&gt;
&lt;p&gt;A couple of key takeaways from working on this issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/carltongibson"&gt;Carlton Gibson&lt;/a&gt; &lt;a href="https://overcast.fm/+QkIrhujD0/21:00"&gt;said&lt;/a&gt; essentially once you start working a ticket from  &lt;a href="https://code.djangoproject.com/"&gt;Trac&lt;/a&gt;, you are the world's foremost export on that ticket ... and he's right!&lt;/li&gt;
&lt;li&gt;... But, you're not working the ticket alone! During the course of my work on the issue I had help from &lt;a href="https://github.com/charettes"&gt;Simon Charette&lt;/a&gt;, &lt;a href="https://github.com/felixxm"&gt;Mariusz Felisiak&lt;/a&gt;, &lt;a href="https://github.com/ngnpope"&gt;Nick Pope&lt;/a&gt;, and &lt;a href="https://github.com/shaib"&gt;Shai Berger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The ORM can seem big and scary ... but remember, it's &lt;em&gt;just&lt;/em&gt; Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think that each of these lesson learned is important for anyone thinking of contributing to Django (or other open source projects).&lt;/p&gt;
&lt;p&gt;That being said, the last point is one that I think can't be emphasized enough.&lt;/p&gt;
&lt;p&gt;The ORM has a reputation for being this big black box that only 'really smart people' can understand and contribute to. But, it really is &lt;em&gt;just&lt;/em&gt; Python.&lt;/p&gt;
&lt;p&gt;If you're using Django, you know (more likely than not) a little bit of Python. Also, if you're using Django, and have written &lt;strong&gt;any&lt;/strong&gt; models, you have a conceptual understanding of what SQL is trying to do (well enough I would argue) that you can get in there AND make sense of what is happening.&lt;/p&gt;
&lt;p&gt;And if you know a little bit of Python a great way to learn more is to get into a project like Django and try to fix a bug.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://code.djangoproject.com/ticket/10070#comment:27"&gt;My initial solution&lt;/a&gt; isn't &lt;a href="https://github.com/django/django/pull/16243"&gt;the final one that got merged&lt;/a&gt; ... it was a collaboration with 4 people, 2 of whom I've never met in real life, and the other 2 I only just met at DjangoCon US a few weeks before.&lt;/p&gt;
&lt;p&gt;While working through this I learned just as much from the feedback on my code as I did from trying to solve the problem with my own code.&lt;/p&gt;
&lt;p&gt;All of this is to say, contributing to open source can be hard, it can be scary, but honestly, I can't think of a better place to start than Django, and there are &lt;a href="https://code.djangoproject.com/query?owner=nobody&amp;amp;status=assigned&amp;amp;status=new&amp;amp;col=id&amp;amp;col=summary&amp;amp;col=owner&amp;amp;col=status&amp;amp;col=component&amp;amp;col=type&amp;amp;col=version&amp;amp;desc=1&amp;amp;order=id"&gt;lots of places to start&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And for those of you feeling a bit adventurous, there are plenty of &lt;a href="https://code.djangoproject.com/query?status=assigned&amp;amp;status=new&amp;amp;owner=nobody&amp;amp;component=Database+layer+(models%2C+ORM)&amp;amp;col=id&amp;amp;col=summary&amp;amp;col=status&amp;amp;col=component&amp;amp;col=owner&amp;amp;col=type&amp;amp;col=version&amp;amp;desc=1&amp;amp;order=id"&gt;ORM&lt;/a&gt; tickets just waiting for you to try and fix them!&lt;/p&gt;</content><category term="technology"></category><category term="django"></category><category term="open source"></category></entry><entry><title>Upgrading to PostgreSQL 14</title><link href="https://ryancheley.com/2022/08/28/upgrading-to-postgresql-14/" rel="alternate"></link><published>2022-08-28T00:00:00-07:00</published><updated>2022-08-28T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-08-28:/2022/08/28/upgrading-to-postgresql-14/</id><summary type="html">&lt;p&gt;&lt;a href="https://docs.djangoproject.com/en/4.1/releases/4.1/"&gt;Django 4.1 was released on August 3, 2022&lt;/a&gt; and I was excited to upgrade to it. I did the testing locally and then pushed my changes up to GitHub to deploy. The deployment was successful, but when I went to visit my sites ... womp womp. I got a Server …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://docs.djangoproject.com/en/4.1/releases/4.1/"&gt;Django 4.1 was released on August 3, 2022&lt;/a&gt; and I was excited to upgrade to it. I did the testing locally and then pushed my changes up to GitHub to deploy. The deployment was successful, but when I went to visit my sites ... womp womp. I got a Server Error 5XX.&lt;/p&gt;
&lt;p&gt;What happened? Well, it turns out that Django 4.1 &lt;a href="https://docs.djangoproject.com/en/4.1/releases/4.1/#dropped-support-for-postgresql-10"&gt;dropped support for Postgres 10&lt;/a&gt; and that just so happens to be the version I was running on my production server (but not on my local dev machine ... I was running Postgres 14).&lt;/p&gt;
&lt;p&gt;OK, so I am going to need to upgrade in order to get the features of anything above Django 4.0 ... and honestly, I've needed to upgrade past Postgres 10 for a &lt;a href="https://www.ryancheley.com/2021/07/09/contributing-to-django-sql-dashboard/"&gt;while&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I found &lt;a href="https://stackoverflow.com/questions/60409585/how-to-upgrade-postgresql-database-from-10-to-12-without-losing-data-for-openpro"&gt;this StackOverflow question and answer&lt;/a&gt; and it helped me a ton! It was to upgrade from Psotgres 10 to 12, but the ideas were the same (but replace 12 with 14). There is also a step that indicates you need to run &lt;code&gt;./analyze_new_cluster.sh&lt;/code&gt; but that seems to be only for version 12(maybe 13) and lower.&lt;/p&gt;
&lt;p&gt;Everything was fine until I visited my site and got a Server Error 5XX AGAIN!&lt;/p&gt;
&lt;p&gt;What gives?&lt;/p&gt;
&lt;p&gt;My first assumption was that maybe the postgres server didn't start back up properly after the upgrade. I checked the service to verify that it was running, and it was&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ps -aux | grep postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which returned&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;988&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;321668&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;27588&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config_file&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1034&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;321788&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;6112&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1035&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;321800&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;5996&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1036&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;321668&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;9388&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;walwriter&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1039&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;322356&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;autovacuum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;launcher&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1040&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;176828&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;5108&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1041&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;322224&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;6628&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Ss&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;logical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;replication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;launcher&lt;/span&gt;
&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="mi"&gt;4868&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;14860&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1072&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;grep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also checked&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl status postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which returned as expected&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostgreSQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RDBMS&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Loaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ago&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1169&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1169&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Aug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostgreSQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RDBMS&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;Aug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Started&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostgreSQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RDBMS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One last thing to try&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python manage.py makemigrations
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gave me a hint as to what the issue was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;RuntimeWarning&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Got&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;checking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;consistent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;migration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;performed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;default&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FATAL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;authentication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FATAL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hmmm ... a quick google search doesn't specifically answer it, but it helps me to get the to answer.&lt;/p&gt;
&lt;p&gt;The 'user' isn't able to connect to the database. Maybe the upgrade process resets the password of users in the database or it just doesn't keep the users.&lt;/p&gt;
&lt;p&gt;A quick look at the users on the database showed me that the users were still there, so the only thing left to do at this point was to set the user passwords to be what my settings are expecting.&lt;/p&gt;
&lt;p&gt;To do that I ran&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;password&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I did this for the databases that were associated with my websites that were returning 5XX errors and voila! That fixed the issue.&lt;/p&gt;
&lt;p&gt;I'm sure that there is a way to keep the passwords for the users after the upgrade, but I haven't been able to find it.&lt;/p&gt;
&lt;p&gt;The next time I need to upgrade PostgreSQL I am going to refer back to this post to remind myself what I did last time 😀&lt;/p&gt;</content><category term="technology"></category><category term="postgres"></category></entry><entry><title>A Goodbye to Vin</title><link href="https://ryancheley.com/2022/08/05/a-goodbye-to-vin/" rel="alternate"></link><published>2022-08-05T00:00:00-07:00</published><updated>2022-08-05T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-08-05:/2022/08/05/a-goodbye-to-vin/</id><summary type="html">&lt;p&gt;One of the earliest memories of my grandmother is visiting her in 29 Palms
&lt;sup id="sf-a-goodbye-to-vin-1-back"&gt;&lt;a href="#sf-a-goodbye-to-vin-1" class="simple-footnote" title="Yes that 29 Palms, right next to the LARGEST Marine Corp Base in the WORLD"&gt;1&lt;/a&gt;&lt;/sup&gt;
&lt;sup id="sf-a-goodbye-to-vin-2-back"&gt;&lt;a href="#sf-a-goodbye-to-vin-2" class="simple-footnote" title="also the 29 Palms that is right next to Joshua Tree home to the National Park that is the current catnip of Hipsters"&gt;2&lt;/a&gt;&lt;/sup&gt; in her permanent mobile home. I remember sitting on the davenport watching the Dodgers on a small 13" COLOR CRT TV. I remember that the game was broadcast on KTLA5. But what I remember …&lt;/p&gt;</summary><content type="html">&lt;p&gt;One of the earliest memories of my grandmother is visiting her in 29 Palms
&lt;sup id="sf-a-goodbye-to-vin-1-back"&gt;&lt;a href="#sf-a-goodbye-to-vin-1" class="simple-footnote" title="Yes that 29 Palms, right next to the LARGEST Marine Corp Base in the WORLD"&gt;1&lt;/a&gt;&lt;/sup&gt;
&lt;sup id="sf-a-goodbye-to-vin-2-back"&gt;&lt;a href="#sf-a-goodbye-to-vin-2" class="simple-footnote" title="also the 29 Palms that is right next to Joshua Tree home to the National Park that is the current catnip of Hipsters"&gt;2&lt;/a&gt;&lt;/sup&gt; in her permanent mobile home. I remember sitting on the davenport watching the Dodgers on a small 13" COLOR CRT TV. I remember that the game was broadcast on KTLA5. But what I remember the most is the voice of Vin Scully.&lt;/p&gt;
&lt;p&gt;I don't know what who the Dodgers were playing, but I remember how much my grandmother LOVED to listen to Vin call the game. And it stuck with me. I was probably about 7 or 8 and I thought baseball was "boring". To be fair, I thought most sports were boring, but especially baseball. Nothing ever happens! But, I loved my grandmother, and I loved hanging out with her &lt;sup id="sf-a-goodbye-to-vin-3-back"&gt;&lt;a href="#sf-a-goodbye-to-vin-3" class="simple-footnote" title="she always had the butter scotch hard candies that were my favorite"&gt;3&lt;/a&gt;&lt;/sup&gt; and so I watched the game with her.&lt;/p&gt;
&lt;p&gt;Years later I discovered that yes, I did like baseball, and no, it was not boring. And since my grandmother was a Dodgers fan, then I would be too. It was something that connected us. it didn't matter where I lived, or how old I was, we both loved baseball. We both loved the Dodgers. We both loved to hear Vin call the game.&lt;/p&gt;
&lt;p&gt;My grandmother died in 2007, but something that helped to connect me to her in the years since was watching the Dodgers. Listening to Vin.&lt;/p&gt;
&lt;p&gt;As Vin got older, he still called the home games, but he handed most of the road games to a new crew. I still loved to Watch Dodgers games, but I loved watching the games he called a &lt;em&gt;little&lt;/em&gt; bit more. At the start of each season I always kind of wondered, "is this the last year for Vin?". And in 2016 the answer was yes.&lt;/p&gt;
&lt;p&gt;I still remember the last game &lt;a href="https://www.espn.com/mlb/game/_/gameId/360925119"&gt;he called in Dodgers Stadium&lt;/a&gt;. I remember the back and forth. I remember the Rockies going up 1 run in the top of the 9th. And the Dodgers tying it back up in the bottom of the 9th. And I remember when &lt;a href="https://youtu.be/HayOXW09kl8"&gt;Charlie Culberson hit the game winning home run in the bottom of the 10th&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I remember the last game &lt;a href="https://www.ryancheley.com/2016/10/03/vins-last-game/"&gt;Vin called in San Francisco&lt;/a&gt;. I remember the Dodgers lost ... but it was Vin's last game, so I still loved getting the chance to watch it. And to listen to him call the game.&lt;/p&gt;
&lt;p&gt;Vin passed at the age of 94 on Aug 2, 2022. Just as I knew that there would be a day when Vin retired from calling games, I knew there would be a day when he wouldn't be with us anymore.&lt;/p&gt;
&lt;p&gt;I've been trying process this and figure out &lt;em&gt;why&lt;/em&gt; this is hitting me as hard as it is.&lt;/p&gt;
&lt;p&gt;It all comes back to my grandmother. They never met each other (at least I don't think they did), but in my head they were inextricably connected. Vin was a connection to my grandmother that I didn't fully realize I had, and with his passing that connection isn't there anymore. He hasn't called a game in more than 5 years, but still, knowing that he NEVER will again is hitting a bit hard for me. And I think it's because it reminds me that my grandma isn't here to watch the games with me anymore, and that bums me out. She was a cool lady who always loved the Dodgers ... and Vin.&lt;/p&gt;
&lt;h1&gt;WinForVin&lt;/h1&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-a-goodbye-to-vin-1"&gt;Yes that 29 Palms, right next to the &lt;a href="https://en.wikipedia.org/wiki/Marine_Corps_Air_Ground_Combat_Center_Twentynine_Palms"&gt;LARGEST Marine Corp Base in the WORLD&lt;/a&gt; &lt;a href="#sf-a-goodbye-to-vin-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-a-goodbye-to-vin-2"&gt;also the 29 Palms that is right next to &lt;a href="https://en.wikipedia.org/wiki/Joshua_Tree,_California"&gt;Joshua Tree&lt;/a&gt; home to the &lt;a href="https://en.wikipedia.org/wiki/Joshua_Tree_National_Park"&gt;National Park&lt;/a&gt; that is the current catnip of Hipsters &lt;a href="#sf-a-goodbye-to-vin-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-a-goodbye-to-vin-3"&gt;she always had the &lt;a href="https://www.candynation.com/butterscotch-candy-buttons"&gt;butter scotch hard candies&lt;/a&gt; that were my favorite &lt;a href="#sf-a-goodbye-to-vin-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category><category term="baseball"></category></entry><entry><title>Django and Legacy Databases</title><link href="https://ryancheley.com/2022/06/15/django-and-legacy-databases/" rel="alternate"></link><published>2022-06-15T00:00:00-07:00</published><updated>2022-06-15T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-06-15:/2022/06/15/django-and-legacy-databases/</id><summary type="html">&lt;p&gt;I work at a place that is heavily investing in the Microsoft Tech Stack. Windows Servers, c#.Net, Angular, VB.net, Windows Work Stations, Microsoft SQL Server ... etc&lt;/p&gt;
&lt;p&gt;When not at work, I &lt;strong&gt;really&lt;/strong&gt; like working with Python and Django. I've never really thought I'd be able to combine the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I work at a place that is heavily investing in the Microsoft Tech Stack. Windows Servers, c#.Net, Angular, VB.net, Windows Work Stations, Microsoft SQL Server ... etc&lt;/p&gt;
&lt;p&gt;When not at work, I &lt;strong&gt;really&lt;/strong&gt; like working with Python and Django. I've never really thought I'd be able to combine the two until I discovered the package mssql-django which was released Feb 18, 2021 in alpha and as a full-fledged version 1 in late July of that same year.&lt;/p&gt;
&lt;p&gt;Ever since then I've been trying to figure out how to incorporate Django into my work life.&lt;/p&gt;
&lt;p&gt;I'm going to use this series as an outline of how I'm working through the process of getting Django to be useful at work. The issues I run into, and the solutions I'm (hopefully) able to achieve.&lt;/p&gt;
&lt;p&gt;I'm also going to use this as a more in depth analysis of an accompanying talk I'm hoping to give at &lt;a href="https://2022.djangocon.us"&gt;Django Con 2022&lt;/a&gt; later this year.&lt;/p&gt;
&lt;p&gt;I'm going to break this down into a several part series that will roughly align with the talk I'm hoping to give. The parts will be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Introduction/Background&lt;/li&gt;
&lt;li&gt;Overview of the Project&lt;/li&gt;
&lt;li&gt;Wiring up the Project Models&lt;/li&gt;
&lt;li&gt;Database Routers&lt;/li&gt;
&lt;li&gt;Django Admin Customization&lt;/li&gt;
&lt;li&gt;Admin Documentation&lt;/li&gt;
&lt;li&gt;Review &amp;amp; Resources&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My intention is to publish one part every week or so. Sometimes the posts will come fast, and other times not. This will mostly be due to how well I'm doing with writing up my findings and/or getting screenshots that will work.&lt;/p&gt;
&lt;p&gt;The tool set I'll be using is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;docker&lt;/li&gt;
&lt;li&gt;docker-compose&lt;/li&gt;
&lt;li&gt;Django&lt;/li&gt;
&lt;li&gt;MS SQL&lt;/li&gt;
&lt;li&gt;SQLite&lt;/li&gt;
&lt;/ul&gt;</content><category term="technology"></category><category term="django"></category><category term="mssql"></category></entry><entry><title>Inserting a URL in Markdown in VS Code</title><link href="https://ryancheley.com/2022/04/08/inserting-a-url-in-markdown-in-vs-code/" rel="alternate"></link><published>2022-04-08T00:00:00-07:00</published><updated>2022-04-08T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-04-08:/2022/04/08/inserting-a-url-in-markdown-in-vs-code/</id><summary type="html">&lt;p&gt;Since I &lt;a href="https://www.ryancheley.com/2021/07/02/migrating-to-pelican-from-
wordpress/"&gt;switched my blog to pelican&lt;/a&gt; last summer I've been using &lt;a href="https://code.visualstudio.com"&gt;VS Code&lt;/a&gt; as my writing app. And it's &lt;strong&gt;really&lt;/strong&gt; good for writing, note just code but prose as well.&lt;/p&gt;
&lt;p&gt;The one problem I've had is there's no keyboard shortcut for links when writing in markdown ... at least not …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Since I &lt;a href="https://www.ryancheley.com/2021/07/02/migrating-to-pelican-from-
wordpress/"&gt;switched my blog to pelican&lt;/a&gt; last summer I've been using &lt;a href="https://code.visualstudio.com"&gt;VS Code&lt;/a&gt; as my writing app. And it's &lt;strong&gt;really&lt;/strong&gt; good for writing, note just code but prose as well.&lt;/p&gt;
&lt;p&gt;The one problem I've had is there's no keyboard shortcut for links when writing in markdown ... at least not a default / native keyboard shortcut.&lt;/p&gt;
&lt;p&gt;In other (macOS) writing apps you just select the text and press ⌘+k and boop! There's a markdown link set up for you. But not so much in VS Code.&lt;/p&gt;
&lt;p&gt;I finally got to the point where that was one thing that may have been keeping me from writing because of how much 'friction' it caused!&lt;/p&gt;
&lt;p&gt;So, I decided to figure out how to fix that.&lt;/p&gt;
&lt;p&gt;I did have to do a bit of googling and eventually found &lt;a href="https://stackoverflow.com/a/70601782"&gt;this&lt;/a&gt; StackOverflow answer&lt;/p&gt;
&lt;p&gt;Essentially the answer is&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the Preferences Page: ⌘+Shift+P&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Preferences: Open Keyboard Shortcuts (JSON)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;keybindings.json&lt;/code&gt; file to include a new key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The new key looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cmd+k&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;command&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;editor.action.insertSnippet&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;snippet&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[${TM_SELECTED_TEXT}]($0)&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;when&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;editorHasSelection &amp;amp;&amp;amp; editorLangId == markdown &amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Honestly, it's &lt;em&gt;little&lt;/em&gt; things like this that can make life so much easier and more fun. Now I just need to remember to do this on my work computer 😀&lt;/p&gt;</content><category term="technology"></category><category term="vscode"></category><category term="shortcuts"></category></entry><entry><title>Logging Part 2</title><link href="https://ryancheley.com/2022/04/07/logging-part-2/" rel="alternate"></link><published>2022-04-07T00:00:00-07:00</published><updated>2022-04-07T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-04-07:/2022/04/07/logging-part-2/</id><summary type="html">&lt;p&gt;In my &lt;a href="https://www.ryancheley.com/2022/03/30/logging-part-1/"&gt;previous post&lt;/a&gt; I wrote about inline logging, that is, using logging in the code without a configuration file of some kind.&lt;/p&gt;
&lt;p&gt;In this post I'm going to go over setting up a configuration file to support the various different needs you may have for logging.&lt;/p&gt;
&lt;p&gt;Previously I mentioned …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In my &lt;a href="https://www.ryancheley.com/2022/03/30/logging-part-1/"&gt;previous post&lt;/a&gt; I wrote about inline logging, that is, using logging in the code without a configuration file of some kind.&lt;/p&gt;
&lt;p&gt;In this post I'm going to go over setting up a configuration file to support the various different needs you may have for logging.&lt;/p&gt;
&lt;p&gt;Previously I mentioned this scenario:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Perhaps the DevOps team wants robust logging messages on anything &lt;code&gt;ERROR&lt;/code&gt; and above, but the application team wants to have &lt;code&gt;INFO&lt;/code&gt; and above in a rotating file name schema, while the QA team needs to have the &lt;code&gt;DEBUG&lt;/code&gt; and up output to standard out.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before we get into how we may implement something like what's above, let's review the parts of the Logger which are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.html#formatter-objects"&gt;formatters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.html#handler-objects"&gt;handlers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.html#logger-objects"&gt;loggers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Formatters&lt;/h2&gt;
&lt;p&gt;In a logging configuration file you can have multiple formatters specified. The above example doesn't state WHAT each team need, so let's define it here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DevOps: They need to know &lt;strong&gt;when&lt;/strong&gt; the error occurred, what the &lt;strong&gt;level&lt;/strong&gt; was, and what &lt;strong&gt;module&lt;/strong&gt; the error came from&lt;/li&gt;
&lt;li&gt;Application Team: They need to know &lt;strong&gt;when&lt;/strong&gt; the error occurred, the &lt;strong&gt;level&lt;/strong&gt;, what &lt;strong&gt;module&lt;/strong&gt; and &lt;strong&gt;line&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The QA Team: They need to know when the error occurred, the &lt;strong&gt;level&lt;/strong&gt;, what &lt;strong&gt;module&lt;/strong&gt; and &lt;strong&gt;line&lt;/strong&gt;, and they need a &lt;strong&gt;stack trace&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the Devops Team we can define a formatter as such&lt;sup id="sf-logging-part-2-1-back"&gt;&lt;a href="#sf-logging-part-2-1" class="simple-footnote" title="full documentation on what is available for the formatters can be found here: https://docs.python.org/3/library/logging.html#logrecord-attributes"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;'%(asctime)s - %(levelname)s - %(module)s'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Application team would have a formatter like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;'%(asctime)s - %(levelname)s - %(module)s - %(lineno)s'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;while the QA team would have one like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;'%(asctime)s - %(levelname)s - %(module)s - %(lineno)s'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Handlers&lt;/h2&gt;
&lt;p&gt;The Handler controls &lt;em&gt;where&lt;/em&gt; the data from the log is going to be sent. There are several kinds of handlers, but based on our requirements above, we'll only be looking at three of them (see the &lt;a href="https://docs.python.org/3/howto/logging.html#useful-handlers"&gt;documentation&lt;/a&gt; for more types of handlers)&lt;/p&gt;
&lt;p&gt;From the example above we know that the DevOps team wants to save the output to a file, while the Application Team wants to have the log data saved in a way that allows the log files to not get &lt;strong&gt;too&lt;/strong&gt; big. Finally, we know that the QA team wants the output to go directly to &lt;code&gt;stdout&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We can handle all of these requirements via the handlers. In this case, we'd use&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.handlers.html#filehandler"&gt;FileHandler&lt;/a&gt; for the DevOps team&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.handlers.html#rotatingfilehandler"&gt;RotatingFileHandler&lt;/a&gt; for the Application team&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/logging.handlers.html#streamhandler"&gt;StreamHandler&lt;/a&gt; for the QA team&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configuration File&lt;/h2&gt;
&lt;p&gt;Above we defined the formatter and handler. Now we start to put them together. The basic format of a logging configuration has 3 parts (as described above). The example I use below is &lt;code&gt;YAML&lt;/code&gt;, but a dictionary or a &lt;code&gt;conf&lt;/code&gt; file would also work.&lt;/p&gt;
&lt;p&gt;Below we see five keys in our &lt;code&gt;YAML&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;formatters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;loggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; key is to allow for future versions in case any are introduced. As of this writing, there is only 1 version ... and it's &lt;code&gt;version: 1&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Formatters&lt;/h3&gt;
&lt;p&gt;We defined the formatters above so let's add them here and give them names that map to the teams&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;formatters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Right off the bat we can see that the formatters for &lt;code&gt;application&lt;/code&gt; and &lt;code&gt;qa&lt;/code&gt; are the same, so we can either keep them separate to help allow for easier updates in the future (and to be more explicit) OR we can merge them into a single formatter to adhere to &lt;code&gt;DRY&lt;/code&gt; principals.&lt;/p&gt;
&lt;p&gt;I'm choosing to go with option 1 and keep them separate.&lt;/p&gt;
&lt;h3&gt;Handlers&lt;/h3&gt;
&lt;p&gt;Next, we add our handlers. Again, we give them names to map to the team. There are several keys for the handlers that are specific to the type of handler that is used. For each handler we set a level (which will map to the level from the specs above).&lt;/p&gt;
&lt;p&gt;Additionally, each handler has keys associated based on the type of handler selected. For example, &lt;code&gt;logging.FileHandler&lt;/code&gt; needs to have the filename specified, while &lt;code&gt;logging.StreamHandler&lt;/code&gt; needs to specify where to output to.&lt;/p&gt;
&lt;p&gt;When using &lt;code&gt;logging.handlers.RotatingFileHandler&lt;/code&gt; we have to specify a few more items in addition to a filename so the logger knows how and when to rotate the log writing.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;formatters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.FileHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ERROR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'devops.log'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.handlers.RotatingFileHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;INFO&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'application.log'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'a'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10000&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;backupCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.StreamHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;DEBUG&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ext://sys.stdout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What the setup above does for the &lt;code&gt;devops&lt;/code&gt; handler is to output the log data to a file called &lt;code&gt;devops.log&lt;/code&gt;, while the &lt;code&gt;application&lt;/code&gt; handler outputs to a rotating set of files called &lt;code&gt;application.log&lt;/code&gt;. For the &lt;code&gt;application.log&lt;/code&gt; it will hold a maximum of 10,000 bytes. Once the file is 'full' it will create a new file called &lt;code&gt;application.log.1&lt;/code&gt;, copy the contents of &lt;code&gt;application.log&lt;/code&gt; and then clear out the contents of &lt;code&gt;application.log&lt;/code&gt; to start over. It will do this 3 times, giving the application team the following files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;application.log&lt;/li&gt;
&lt;li&gt;application.log.1&lt;/li&gt;
&lt;li&gt;application.log.2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, the handler for QA will output directly to stdout.&lt;/p&gt;
&lt;h3&gt;Loggers&lt;/h3&gt;
&lt;p&gt;Now we can take all of the work we did above to create the &lt;code&gt;formatters&lt;/code&gt; and &lt;code&gt;handlers&lt;/code&gt; and use them in the &lt;code&gt;loggers&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Below we see how the loggers are set up in configuration file. It seems a &lt;em&gt;bit&lt;/em&gt; redundant because I've named my formatters, handlers, and loggers all matching terms, but 🤷‍♂️&lt;/p&gt;
&lt;p&gt;The only new thing we see in the configuration below is the new &lt;code&gt;propagate: no&lt;/code&gt; for each of the loggers. If there were parent loggers (we don't have any) then this would prevent the logging information from being sent 'up' the chain to parent loggers.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.python.org/3/howto/logging.html#logging-flow"&gt;documentation&lt;/a&gt; has a good diagram showing the workflow for how the &lt;code&gt;propagate&lt;/code&gt; works.&lt;/p&gt;
&lt;p&gt;Below we can see what the final, fully formed logging configuration looks like.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;formatters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'%(asctime)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(levelname)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(module)s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(lineno)s'&lt;/span&gt;
&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.FileHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ERROR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'devops.log'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.handlers.RotatingFileHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;INFO&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'application.log'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'a'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10000&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;backupCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;logging.StreamHandler&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;DEBUG&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ext://sys.stdout&lt;/span&gt;
&lt;span class="nt"&gt;loggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;devops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ERROR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;devops&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;devops&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;no&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;INFO&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;application&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;no&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;qa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;DEBUG&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;qa&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;qa&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;no&lt;/span&gt;
&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ERROR&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;devops&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;qa&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In my next post I'll write about how to use the above configuration file to allow the various teams to get the log output they need.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-logging-part-2-1"&gt;full documentation on what is available for the formatters can be found here: https://docs.python.org/3/library/logging.html#logrecord-attributes &lt;a href="#sf-logging-part-2-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="logging"></category><category term="python"></category></entry><entry><title>Logging Part 1</title><link href="https://ryancheley.com/2022/03/30/logging-part-1/" rel="alternate"></link><published>2022-03-30T00:00:00-07:00</published><updated>2022-03-30T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-03-30:/2022/03/30/logging-part-1/</id><summary type="html">&lt;h1&gt;Logging&lt;/h1&gt;
&lt;p&gt;Last year I worked on an update to the package &lt;a href="https://pypi.org/project/tryceratops/"&gt;tryceratops&lt;/a&gt; with &lt;a href="https://twitter.com/guilatrova"&gt;Gui Latrova&lt;/a&gt; to include a verbose flag for logging.&lt;/p&gt;
&lt;p&gt;Honestly, Gui was a huge help and I wrote about my experience &lt;a href="[link](https://www.ryancheley.com/2021/08/07/contributing-to-tryceratops/)"&gt;here&lt;/a&gt; but I didn't really understand why what I did worked.&lt;/p&gt;
&lt;p&gt;Recently I decided that I …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;Logging&lt;/h1&gt;
&lt;p&gt;Last year I worked on an update to the package &lt;a href="https://pypi.org/project/tryceratops/"&gt;tryceratops&lt;/a&gt; with &lt;a href="https://twitter.com/guilatrova"&gt;Gui Latrova&lt;/a&gt; to include a verbose flag for logging.&lt;/p&gt;
&lt;p&gt;Honestly, Gui was a huge help and I wrote about my experience &lt;a href="[link](https://www.ryancheley.com/2021/08/07/contributing-to-tryceratops/)"&gt;here&lt;/a&gt; but I didn't really understand why what I did worked.&lt;/p&gt;
&lt;p&gt;Recently I decided that I wanted to better understand logging so I dove into some posts from Gui, and sat down and read the documentation on the logging from the standard library.&lt;/p&gt;
&lt;p&gt;My goal with this was to (1) be able to use logging in my projects, and (2) write something that may be able to help others.&lt;/p&gt;
&lt;p&gt;Full disclosure, Gui has a &lt;strong&gt;really&lt;/strong&gt; &lt;a href="https://guicommits.com/how-to-log-in-python-like-a-pro/"&gt;good article explaining logging&lt;/a&gt; and I think everyone should read it. My notes below are a synthesis of his article, my understanding of the &lt;a href="https://docs.python.org/3/library/logging.html"&gt;documentation from the standard library&lt;/a&gt;, and the &lt;a href="https://docs.python.org/3/howto/logging.html"&gt;Python HowTo&lt;/a&gt; written in a way to answer the &lt;a href="https://www.education.com/game/five-ws-song/"&gt;Five W questions&lt;/a&gt; I was taught in grade school.&lt;/p&gt;
&lt;h2&gt;The Five W's&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Who are the generated logs for?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Anyone trying to troubleshoot an issue, or monitor the history of actions that have been logged in an application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is written to the log?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.python.org/3/library/logging.html#formatter-objects"&gt;formatter&lt;/a&gt; determines what to display or store.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When is data written to the log?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.python.org/3/library/logging.html#logging-levels"&gt;logging level&lt;/a&gt; determines when to log the issue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where is the log data sent to?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.python.org/3/library/logging.html#handler-objects"&gt;handler&lt;/a&gt; determines where to send the log data whether that's a file, or stdout.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why would I want to use logging?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To keep a history of actions taken during your code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How is the data sent to the log?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.python.org/3/library/logging.html#logger-objects"&gt;loggers&lt;/a&gt; determine how to bundle all of it together through calls to various methods.&lt;/p&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;p&gt;Let's say I want a logger called &lt;code&gt;my_app_errors&lt;/code&gt; that captures all ERROR level incidents and higher to a file and to tell me the date time, level, message, logger name, and give a trace back of the error, I could do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;oh no! an error occurred&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(levelname)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(name)s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my_app_errors&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;errors.log&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code above would generate something like this to a file called &lt;code&gt;errors.log&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;03&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;188&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;oh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;occurred&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;my_app_errors&lt;/span&gt;
&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/Users/ryan/Documents/github/logging/test.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="n"&gt;ger&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stack_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If I want a logger that will do all of the above AND output debug information to the console I could:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;oh no! an error occurred&amp;#39;&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my_app_errors&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;errors.log&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(levelname)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s1"&gt; - &lt;/span&gt;&lt;span class="si"&gt;%(name)s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Again, the code above would generate something like this to a file called &lt;code&gt;errors.log&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;03&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;406&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;oh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;occurred&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;my_app_errors&lt;/span&gt;
&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/Users/ryan/Documents/github/logging/test.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="n"&gt;ger&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stack_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;but it would also output to stderr in the terminal something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;03&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;367&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;oh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;occurred&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;my_app_errors&lt;/span&gt;
&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The above it a bit hard to scale though. What happens when we want to have multiple formatters, for different levels that get output to different places? We can incorporate all of that into something like what we see above, OR, we can stat to leverage the use of logging configuration files.&lt;/p&gt;
&lt;p&gt;Why would we want to have multiple formatters? Perhaps the DevOps team wants robust logging messages on anything &lt;code&gt;ERROR&lt;/code&gt; and above, but the application team wants to have &lt;code&gt;INFO&lt;/code&gt; and above in a rotating file name schema, while the QA team needs to have the &lt;code&gt;DEBUG&lt;/code&gt; and up output to standard out.&lt;/p&gt;
&lt;p&gt;You CAN do all of this inline with the code above, but would you &lt;strong&gt;really&lt;/strong&gt; want to? Probably not.&lt;/p&gt;
&lt;p&gt;Enter configuration files to allow easier management of log files (and a potential way to make everyone happy) which I'll cover in the next post.&lt;/p&gt;</content><category term="technology"></category><category term="logging"></category><category term="python"></category></entry><entry><title>New Theme, who dis?</title><link href="https://ryancheley.com/2022/02/27/new-theme-who-dis/" rel="alternate"></link><published>2022-02-27T00:00:00-08:00</published><updated>2022-02-27T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-02-27:/2022/02/27/new-theme-who-dis/</id><summary type="html">&lt;p&gt;Because I have a couple of posts that I need/want to work on, and I have the time to work on them, I have of course decided to instead to update the theme on my blog because that was a way better use of my time 😂&lt;/p&gt;
&lt;p&gt;Also, because the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Because I have a couple of posts that I need/want to work on, and I have the time to work on them, I have of course decided to instead to update the theme on my blog because that was a way better use of my time 😂&lt;/p&gt;
&lt;p&gt;Also, because the day is just too nice to not be sitting outside watching baseball (even if it's on TV ... and even if it's the &lt;strong&gt;ping&lt;/strong&gt; of the bat and not the &lt;strong&gt;crack&lt;/strong&gt; of the bat&lt;sup id="sf-new-theme-who-dis-1-back"&gt;&lt;a href="#sf-new-theme-who-dis-1" class="simple-footnote" title="Since the MLB Lockout is still going on and there's no end in sight, I've resorted to watching NCAA Baseball. I have to say, it's really entertaining AND it seems like there's 100 games on each day!"&gt;1&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-new-theme-who-dis-1"&gt;Since the MLB Lockout is &lt;strong&gt;still&lt;/strong&gt; going on and there's &lt;a href="https://www.espn.com/mlb/story/_/id/33347425"&gt;no end in sight&lt;/a&gt;, I've resorted to watching NCAA Baseball. I have to say, it's really entertaining AND it seems like there's 100 games on each day! &lt;a href="#sf-new-theme-who-dis-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="musings"></category></entry><entry><title>I made a Slackbot!</title><link href="https://ryancheley.com/2022/02/19/i-made-a-slackbot/" rel="alternate"></link><published>2022-02-19T00:00:00-08:00</published><updated>2022-02-19T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-02-19:/2022/02/19/i-made-a-slackbot/</id><summary type="html">&lt;h2&gt;Building my first Slack Bot&lt;/h2&gt;
&lt;p&gt;I had added a project to my OmniFocus database in November of 2021 which was, "Build a Slackbot" after watching a &lt;a href="https://www.youtube.com/watch?v=2X8SrKL7E9A"&gt;Video&lt;/a&gt; by &lt;a href="https://twitter.com/masonegger"&gt;Mason Egger&lt;/a&gt;. I had hoped that I would be able to spend some time on it over the holidays, but I was …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Building my first Slack Bot&lt;/h2&gt;
&lt;p&gt;I had added a project to my OmniFocus database in November of 2021 which was, "Build a Slackbot" after watching a &lt;a href="https://www.youtube.com/watch?v=2X8SrKL7E9A"&gt;Video&lt;/a&gt; by &lt;a href="https://twitter.com/masonegger"&gt;Mason Egger&lt;/a&gt;. I had hoped that I would be able to spend some time on it over the holidays, but I was never able to really find the time.&lt;/p&gt;
&lt;p&gt;A few weeks ago, &lt;a href="https://twitter.com/bbelderbos"&gt;Bob Belderbos&lt;/a&gt; tweeted:&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;If you were to build a Slack bot, what would it do?&lt;/p&gt;— Bob Belderbos (@bbelderbos) &lt;a href="https://twitter.com/bbelderbos/status/1488806429251313666?ref_src=twsrc%5Etfw"&gt;February 2, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

&lt;p&gt;And I responded&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;I work in US Healthcare where there are a lot of Acronyms (many of which are used in tech but have different meaning), so my slack bot would allow a user to enter an acronym and return what it means, i.e., CMS = Centers for Medicare and Medicaid Services.&lt;/p&gt;— The B Is Silent (@ryancheley) &lt;a href="https://twitter.com/ryancheley/status/1488879253911261184?ref_src=twsrc%5Etfw"&gt;February 2, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

&lt;p&gt;I didn't &lt;em&gt;really&lt;/em&gt; have anymore time now than I did over the holiday, but Bob asking and me answering pushed me to &lt;em&gt;actually&lt;/em&gt; write the darned thing.&lt;/p&gt;
&lt;p&gt;I think one of the problems I encountered was what backend / tech stack to use. I'm familiar with Django, but going from 0 to something in production has a few steps and although I know how to do them ... I just felt ~overwhelmed~ by the prospect.&lt;/p&gt;
&lt;p&gt;I felt equally ~overwhelmed~ by the prospect of trying FastAPI to create the API or Flask, because I am not as familiar with their deployment story.&lt;/p&gt;
&lt;p&gt;Another thing that was different now than before was that I had worked on a &lt;a href="https://github.com/ryancheley/django-cookiecutter"&gt;Django Cookie Cutter&lt;/a&gt; to use and that was 'good enough' to try it out. So I did.&lt;/p&gt;
&lt;p&gt;I ran into a few &lt;a href="https://github.com/ryancheley/django-cookiecutter/compare/de07ba6..cd7c272"&gt;problems&lt;/a&gt; while working with my Django Cookie Cutter but I fixed them and then dove head first into writing the Slack Bot&lt;/p&gt;
&lt;h2&gt;The model&lt;/h2&gt;
&lt;p&gt;The initial implementation of the model was very simple ... just 2 fields:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Acronym&lt;/span&gt;(&lt;span class="n"&gt;models&lt;/span&gt;.&lt;span class="n"&gt;Model&lt;/span&gt;):
    &lt;span class="n"&gt;acronym&lt;/span&gt; = &lt;span class="n"&gt;models&lt;/span&gt;.&lt;span class="n"&gt;CharField&lt;/span&gt;(&lt;span class="n"&gt;max_length&lt;/span&gt;=&lt;span class="mi"&gt;8&lt;/span&gt;)
    &lt;span class="n"&gt;definition&lt;/span&gt; = &lt;span class="n"&gt;models&lt;/span&gt;.&lt;span class="n"&gt;TextField&lt;/span&gt;()

    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt;(&lt;span class="nb"&gt;self&lt;/span&gt;, *&lt;span class="nb"&gt;args&lt;/span&gt;, **&lt;span class="n"&gt;kwargs&lt;/span&gt;):
        &lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;acronym&lt;/span&gt; = &lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;acronym&lt;/span&gt;.&lt;span class="n"&gt;lower&lt;/span&gt;()
        &lt;span class="n"&gt;super&lt;/span&gt;(&lt;span class="n"&gt;Acronym&lt;/span&gt;, &lt;span class="nb"&gt;self&lt;/span&gt;).&lt;span class="n"&gt;save&lt;/span&gt;(*&lt;span class="nb"&gt;args&lt;/span&gt;, **&lt;span class="n"&gt;kwargs&lt;/span&gt;)

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Meta:&lt;/span&gt;
        &lt;span class="n"&gt;unique_together&lt;/span&gt; = (&lt;span class="s"&gt;"acronym"&lt;/span&gt;, &lt;span class="s"&gt;"definition"&lt;/span&gt;)
        &lt;span class="n"&gt;ordering&lt;/span&gt; = [&lt;span class="s"&gt;"acronym"&lt;/span&gt;]

    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;__str__&lt;/span&gt;(&lt;span class="nb"&gt;self&lt;/span&gt;) -&amp;gt; &lt;span class="n"&gt;str:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;acronym&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next I created the API using &lt;a href="https://www.django-rest-framework.org"&gt;Django Rest Framework&lt;/a&gt; using a single &lt;code&gt;serializer&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;AcronymSerializer&lt;/span&gt;(&lt;span class="n"&gt;serializers&lt;/span&gt;.&lt;span class="n"&gt;ModelSerializer&lt;/span&gt;):
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Meta:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; = &lt;span class="n"&gt;Acronym&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; = [
            &lt;span class="s"&gt;"id"&lt;/span&gt;,
            &lt;span class="s"&gt;"acronym"&lt;/span&gt;,
            &lt;span class="s"&gt;"definition"&lt;/span&gt;,
        ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which is used by a single &lt;code&gt;view&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;(&lt;span class="n"&gt;viewsets&lt;/span&gt;.&lt;span class="n"&gt;ReadOnlyModelViewSet&lt;/span&gt;):
    &lt;span class="n"&gt;serializer_class&lt;/span&gt; = &lt;span class="n"&gt;AcronymSerializer&lt;/span&gt;
    &lt;span class="n"&gt;queryset&lt;/span&gt; = &lt;span class="n"&gt;Acronym&lt;/span&gt;.&lt;span class="n"&gt;objects&lt;/span&gt;.&lt;span class="nb"&gt;all&lt;/span&gt;()

    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;get_object&lt;/span&gt;(&lt;span class="nb"&gt;self&lt;/span&gt;):
        &lt;span class="n"&gt;queryset&lt;/span&gt; = &lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;filter_queryset&lt;/span&gt;(&lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;get_queryset&lt;/span&gt;())
        &lt;span class="nb"&gt;print&lt;/span&gt;(&lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;kwargs&lt;/span&gt;[&lt;span class="s"&gt;"acronym"&lt;/span&gt;])
        &lt;span class="n"&gt;acronym&lt;/span&gt; = &lt;span class="nb"&gt;self&lt;/span&gt;.&lt;span class="n"&gt;kwargs&lt;/span&gt;[&lt;span class="s"&gt;"acronym"&lt;/span&gt;]
        &lt;span class="n"&gt;obj&lt;/span&gt; = &lt;span class="n"&gt;get_object_or_404&lt;/span&gt;(&lt;span class="n"&gt;queryset&lt;/span&gt;, &lt;span class="n"&gt;acronym__iexact&lt;/span&gt;=&lt;span class="n"&gt;acronym&lt;/span&gt;)

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and exposed on 2 end points:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.views&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AddAcronym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CountAcronyms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Events&lt;/span&gt;

&lt;span class="n"&gt;app_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt;

&lt;span class="n"&gt;user_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;user_detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"retrieve"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"acronym-list"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;acronym&amp;gt;/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AcronymViewSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"retrieve"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"acronym-detail"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api-auth/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rest_framework.urls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rest_framework"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Getting the data&lt;/h2&gt;
&lt;p&gt;At my joby-job we use Jira and Confluence. In one of our Confluence spaces we have a Glossary page which includes nearly 200 acronyms. I had two choices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy and Paste the acronym and definition for each item&lt;/li&gt;
&lt;li&gt;Use Python to get the data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I used Python to get the data, via a Jupyter Notebook, but I didn't seem to save the code anywhere (🤦🏻), so I can't include it here. But trust me, it was 💯.&lt;/p&gt;
&lt;h2&gt;Setting up the Slack Bot&lt;/h2&gt;
&lt;p&gt;Although I had watched Mason's video, since I was building this with Django I used &lt;a href="https://medium.com/freehunch/how-to-build-a-slack-bot-with-python-using-slack-events-api-django-under-20-minute-code-included-269c3a9bf64e"&gt;this article&lt;/a&gt; as a guide in the development of the code below.&lt;/p&gt;
&lt;p&gt;The code from my &lt;code&gt;views.py&lt;/code&gt; is below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;ssl_context&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;ssl_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;check_hostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;False&lt;/span&gt;
&lt;span class="nx"&gt;ssl_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify_mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CERT_NONE&lt;/span&gt;

&lt;span class="nx"&gt;SLACK_VERIFICATION_TOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"SLACK_VERIFICATION_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;SLACK_BOT_USER_TOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"SLACK_BOT_USER_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;CONFLUENCE_LINK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"CONFLUENCE_LINK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SLACK_BOT_USER_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;ssl_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APIView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SLACK_VERIFICATION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP_403_FORBIDDEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"url_verification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"event"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;event_message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slack_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ignore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;own&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"subtype"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"https://slackbot.ryancheley.com/api/{text}/"&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"definition"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"The acronym '{text.upper()}' means: {definition}"&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;confluence&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CONFLUENCE_LINK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dosearchsite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="nx"&gt;cql&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;siteSearch&lt;/span&gt;&lt;span class="o"&gt;+~+&lt;/span&gt;&lt;span class="s"&gt;"{text}"&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;confluence_link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;{confluence}|Confluence&amp;gt;"&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"I'm sorry &amp;lt;@{user}&amp;gt; I don't know what *{text.upper()}* is :shrug:. Try checking {confluence_link}."&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"U031T0UHLH1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat_postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;=[{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}}],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Essentially what the Slack Bot does is takes in the &lt;code&gt;request.data['text']&lt;/code&gt; and checks it against the DRF API end point to see if there is a matching Acronym.&lt;/p&gt;
&lt;p&gt;If there is, then it returns the acronym and it's definition.&lt;/p&gt;
&lt;p&gt;If it's not, you get a message that it's not sure what you're looking for, but that maybe Confluence&lt;sup id="sf-i-made-a-slackbot-1-back"&gt;&lt;a href="#sf-i-made-a-slackbot-1" class="simple-footnote" title="You'll notice that I'm using an environment variable to define the Confluence Link and may wonder why. It's mostly to keep the actual Confluence Link used at work non-public and not for any other reason 🤷🏻"&gt;1&lt;/a&gt;&lt;/sup&gt; can help, and gives a link to our Confluence Search page.&lt;/p&gt;
&lt;p&gt;The last thing you'll notice is that if the User has a specific ID it won't respond with a message. That's because in my initial testing I just had the Slack Bot replying to the user saying 'Hi' with a 'Hi' back to the user.&lt;/p&gt;
&lt;p&gt;I had a missing bit of logic though, so once you said hi to the Slack Bot, it would reply back 'Hi' and then keep replying 'Hi' because it was talking to itself. It was comical to see in real time 😂.&lt;/p&gt;
&lt;h2&gt;Using ngrok to test it locally&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://ngrok.com"&gt;&lt;code&gt;ngrok&lt;/code&gt;&lt;/a&gt; is a great tool for taking a local url, like  &lt;a href="localhost:8000/api/entpoint"&gt;localhost:8000/api/entpoint&lt;/a&gt;, and exposing it on the internet with a url like &lt;a href="https://a123-45-678-901-234.ngrok.io/api/entpoint"&gt;https://a123-45-678-901-234.ngrok.io/api/entpoint&lt;/a&gt;. This allows you to test your local code and see any issues that might arise when pushed to production.&lt;/p&gt;
&lt;p&gt;As I mentioned above the Slack Bot continually said "Hi" to itself in my initial testing. Since I was running ngrok to serve up my local Server I was able to stop the infinite loop by stopping my local web server. This would have been a little more challenging if I had to push my code to an actual web server first and &lt;strong&gt;then&lt;/strong&gt; tested.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This was such a fun project to work on, and I'm really glad that &lt;a href="https://twitter.com/bbelderbos"&gt;Bob&lt;/a&gt; tweeted asking what Slack Bot we would build.&lt;/p&gt;
&lt;p&gt;That gave me the final push to actually build it.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-i-made-a-slackbot-1"&gt;You'll notice that I'm using an environment variable to define the Confluence Link and may wonder why. It's mostly to keep the actual Confluence Link used at work non-public and not for any other reason 🤷🏻 &lt;a href="#sf-i-made-a-slackbot-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="technology"></category><category term="django"></category><category term="drf"></category><category term="slack"></category></entry><entry><title>Putting it All Together</title><link href="https://ryancheley.com/2022/02/09/putting-it-all-together/" rel="alternate"></link><published>2022-02-09T00:00:00-08:00</published><updated>2022-02-09T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-02-09:/2022/02/09/putting-it-all-together/</id><summary type="html">&lt;p&gt;In this final post I'll be writing up how everything fits together. As a recap, here are the steps I go through to create and publish a new post&lt;/p&gt;
&lt;h1&gt;Create Post&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;.md&lt;/code&gt; for my new post&lt;/li&gt;
&lt;li&gt;write my words&lt;/li&gt;
&lt;li&gt;edit post&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;status&lt;/code&gt; from &lt;code&gt;draft&lt;/code&gt; to &lt;code&gt;published&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Publish Post …&lt;/h2&gt;</summary><content type="html">&lt;p&gt;In this final post I'll be writing up how everything fits together. As a recap, here are the steps I go through to create and publish a new post&lt;/p&gt;
&lt;h1&gt;Create Post&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;.md&lt;/code&gt; for my new post&lt;/li&gt;
&lt;li&gt;write my words&lt;/li&gt;
&lt;li&gt;edit post&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;status&lt;/code&gt; from &lt;code&gt;draft&lt;/code&gt; to &lt;code&gt;published&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Publish Post&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-putting-it-all-together-1-back"&gt;&lt;a href="#sf-putting-it-all-together-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to be committed to GitHub&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git commit -m &amp;lt;message&amp;gt;&lt;/code&gt; to commit to GitHub&lt;/li&gt;
&lt;li&gt;Post to Twitter with a link to my new post&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My previous posts have gone over how each step was automated, but now we'll 'throw it all together'.&lt;/p&gt;
&lt;p&gt;I updated my &lt;code&gt;Makefile&lt;/code&gt; with a new command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tweet:
    ./tweet.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When I run &lt;code&gt;make tweet&lt;/code&gt; it will calls &lt;code&gt;tweet.sh&lt;/code&gt;. I wrote about the &lt;code&gt;tweet.sh&lt;/code&gt; file in &lt;a href="https://www.ryancheley.com/2022/01/28/auto-generating-the-commit-message/"&gt;Auto Generating the Commit Message&lt;/a&gt; so I won't go deeply into here. What it does is automate steps 1 - 5 above for the &lt;code&gt;Publish Post&lt;/code&gt; section above.&lt;/p&gt;
&lt;p&gt;And that's it really. I've now been able to automate the file creation and publish process.&lt;/p&gt;
&lt;p&gt;Admittedly these are the 'easy' parts. The hard part is the actual writing, but it does remove a ton pf potential friction from my workflow and this will &lt;strong&gt;hopefully&lt;/strong&gt; lead to more writing this year.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-putting-it-all-together-1"&gt;&lt;code&gt;make vercel&lt;/code&gt; actually runs &lt;code&gt;make html&lt;/code&gt; so this isn't really a step that I need to do. &lt;a href="#sf-putting-it-all-together-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="productivity"></category><category term="Automation"></category></entry><entry><title>Automating the file creation</title><link href="https://ryancheley.com/2022/02/02/automating-the-file-creation/" rel="alternate"></link><published>2022-02-02T00:00:00-08:00</published><updated>2022-02-09T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-02-02:/2022/02/02/automating-the-file-creation/</id><summary type="html">&lt;p&gt;In my last post &lt;a href="https://www.ryancheley.com/2022/01/28/auto-generating-the-commit-message/"&gt;Auto Generating the Commit Message&lt;/a&gt; I indicated that this post I would "throw it all together and to get a spot where I can run one make command that will do all of this for me".&lt;/p&gt;
&lt;p&gt;I decided to take a brief detour though as I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In my last post &lt;a href="https://www.ryancheley.com/2022/01/28/auto-generating-the-commit-message/"&gt;Auto Generating the Commit Message&lt;/a&gt; I indicated that this post I would "throw it all together and to get a spot where I can run one make command that will do all of this for me".&lt;/p&gt;
&lt;p&gt;I decided to take a brief detour though as I realized I didn't have a good way to create a new post, i.e. the starting point wasn't automated!&lt;/p&gt;
&lt;p&gt;In this post I'm going to go over how I create the start to a new post using &lt;code&gt;Makefile&lt;/code&gt; and the command &lt;code&gt;make newpost&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My initial idea was to create a new bash script (similar to the &lt;code&gt;tweet.sh&lt;/code&gt; file), but as a first iteration I went in a different direction based on this post &lt;a href="https://blog.codeselfstudy.com/blog/how-to-slugify-strings-in-bash/"&gt;How to Slugify Strings in Bash&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The command that the is finally arrived at in the post above was&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;newpost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s1"&gt;':r templates/post.md'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASEDIR&lt;/span&gt;&lt;span class="o"&gt;)/&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="sr"&gt;/blog/$$(date +%Y-%m-%d)-$$(echo -n $${title} | sed -e 's/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which was &lt;strong&gt;really&lt;/strong&gt; close to what I needed. My static site is set up a bit differently and I'm not using &lt;code&gt;vim&lt;/code&gt; (I'm using VS Code) to write my words.&lt;/p&gt;
&lt;p&gt;The first change I needed to make was to remove the use of &lt;code&gt;vim&lt;/code&gt; from the command and instead use &lt;code&gt;touch&lt;/code&gt; to create the file&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;newpost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASEDIR&lt;/span&gt;&lt;span class="o"&gt;)/&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="sr"&gt;/blog/$$(date +%Y-%m-%d)-$$(echo -n $${title} | sed -e 's/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The second was to change the file path for where to create the file. As I've indicated previously, the structure of my content looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;content
├── musings
├── pages
├── productivity
├── professional\ development
└── technology
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;giving me an updated version of the command that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;touch&lt;span class="w"&gt; &lt;/span&gt;content/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${category})/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${title}&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;A-Z&lt;span class="w"&gt; &lt;/span&gt;a-z.md).md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When I run the command &lt;code&gt;make newpost title='Automating the file creation' category='productivity'&lt;/code&gt; I get a empty new files created.&lt;/p&gt;
&lt;p&gt;Now I just need to populate it with the data.&lt;/p&gt;
&lt;p&gt;There are seven bits of meta data that need to be added, but four of them are the same for each post&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;
&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Needed&lt;/span&gt;
&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That allows me to have the &lt;code&gt;newpost&lt;/code&gt; command look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;newpost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="sr"&gt;/$$(echo $${category})/$$(echo $${title} | sed -e 's/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Author: ryan" &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Tags: " &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Series: Remove if Not Needed"  &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Status: draft"  &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The remaining metadata to be added are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Title:&lt;/li&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Slug&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of these, &lt;code&gt;Date&lt;/code&gt; and &lt;code&gt;Title&lt;/code&gt; are the most straightforward.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bash&lt;/code&gt; has a command called &lt;code&gt;date&lt;/code&gt; that can be formatted in the way I want with &lt;code&gt;%F&lt;/code&gt;. Using this I can get the date like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;echo&lt;span class="w"&gt; &lt;/span&gt;"Date:&lt;span class="w"&gt; &lt;/span&gt;$$(date&lt;span class="w"&gt; &lt;/span&gt;+%F)"&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;content/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${category})/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${title}&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;A-Z&lt;span class="w"&gt; &lt;/span&gt;a-z.md).md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code&gt;Title&lt;/code&gt; I can take the input parameter &lt;code&gt;title&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;echo&lt;span class="w"&gt; &lt;/span&gt;"Title:&lt;span class="w"&gt; &lt;/span&gt;$${title}"&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;content/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${category})/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${title}&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;A-Z&lt;span class="w"&gt; &lt;/span&gt;a-z.md).md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Slug&lt;/code&gt; is just &lt;code&gt;Title&lt;/code&gt; but &lt;em&gt;slugified&lt;/em&gt;. Trying to figure out how to do this is how I found the &lt;a href="https://blog.codeselfstudy.com/blog/how-to-slugify-strings-in-bash/"&gt;article&lt;/a&gt; above.&lt;/p&gt;
&lt;p&gt;Using a slightly modified version of the code that generates the file, we get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;printf&lt;span class="w"&gt; &lt;/span&gt;"Slug:&lt;span class="w"&gt; &lt;/span&gt;"&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;content/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${category})/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${title}&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;A-Z&lt;span class="w"&gt; &lt;/span&gt;a-z.md).md
echo&lt;span class="w"&gt; &lt;/span&gt;"$${title}"&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;content/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${category})/$$(echo&lt;span class="w"&gt; &lt;/span&gt;$${title}&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;'s/[^[:alnum:]]/-/g'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;'-'&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;A-Z&lt;span class="w"&gt; &lt;/span&gt;a-z.md).md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One thing to notice here is that &lt;code&gt;printf&lt;/code&gt;. I needed/wanted to &lt;code&gt;echo -n&lt;/code&gt; but &lt;code&gt;make&lt;/code&gt; didn't seem to like that. &lt;a href="https://stackoverflow.com/a/14121245"&gt;This StackOverflow answer&lt;/a&gt; helped me to get a fix (using &lt;code&gt;printf&lt;/code&gt;) though I'm sure there's a way I can get it to work with &lt;code&gt;echo -n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Essentially, since this was a first pass, and I'm pretty sure I'm going to end up re-writing this as a shell script I didn't want to spend &lt;strong&gt;too&lt;/strong&gt; much time getting a perfect answer here.&lt;/p&gt;
&lt;p&gt;OK, with all of that, here's the entire &lt;code&gt;newpost&lt;/code&gt; recipe I'm using now:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;newpost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="sr"&gt;/$$(echo $${category})/$$(echo $${title} | sed -e 's/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Title: $${title}" &amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Date: $$(date +%F)" &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Author: ryan" &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Tags: " &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    printf "Slug: " &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "$${title}" | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g' | tr -s '-' | tr A-Z a-z &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e 's/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Series: Remove if Not Needed"  &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;' | tr A-Z a-z.md).md&lt;/span&gt;
&lt;span class="s1"&gt;    echo "Status: draft"  &amp;gt;&amp;gt; content/$$(echo $${category})/$$(echo $${title} | sed -e '&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="sr"&gt;/[^[:alnum:]]/-/g&lt;/span&gt;&lt;span class="s1"&gt;' | tr -s '&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This allows me to type &lt;code&gt;make newpost&lt;/code&gt; and generate a new file for me to start my new post in!&lt;sup id="sf-automating-the-file-creation-1-back"&gt;&lt;a href="#sf-automating-the-file-creation-1" class="simple-footnote" title="When this post was originally published the slug command didn't account for making all of the text lower case. This was fixed in a subsequent commit"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-automating-the-file-creation-1"&gt;When this post was originally published the slug command didn't account for making all of the text lower case. This was fixed in a subsequent &lt;a href="https://github.com/ryancheley/ryancheley.com/commit/54f41680fdca4131735346764048d4e5fd206fd6"&gt;commit&lt;/a&gt; &lt;a href="#sf-automating-the-file-creation-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="productivity"></category><category term="automation"></category><category term="makefile"></category></entry><entry><title>Auto Generating the Commit Message</title><link href="https://ryancheley.com/2022/01/28/auto-generating-the-commit-message/" rel="alternate"></link><published>2022-01-28T00:00:00-08:00</published><updated>2022-01-28T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-01-28:/2022/01/28/auto-generating-the-commit-message/</id><summary type="html">&lt;p&gt;In my first post of this series I outlined the steps needed in order for me to post. They are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-auto-generating-the-commit-message-1-back"&gt;&lt;a href="#sf-auto-generating-the-commit-message-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/26/git-add-filename-automation/"&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to …&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;In my first post of this series I outlined the steps needed in order for me to post. They are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-auto-generating-the-commit-message-1-back"&gt;&lt;a href="#sf-auto-generating-the-commit-message-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/26/git-add-filename-automation/"&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to be committed to GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/28/auto-generating-the-commit-message"&gt;Run &lt;code&gt;git commit -m &amp;lt;message&amp;gt;&lt;/code&gt; to commit to GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/24/auto-tweeting-new-post/"&gt;Post to Twitter with a link to my new post&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this post I'll be focusing on how I automated step 4, Run &lt;code&gt;git commit -m &amp;lt;message&amp;gt;&lt;/code&gt; to commit to GitHub.&lt;/p&gt;
&lt;h1&gt;Automating the "git commit ..." part of my workflow&lt;/h1&gt;
&lt;p&gt;In order for my GitHub Action to auto post to Twitter, my commit message needs to be in the form of "New Post: ...". What I'm looking for is to be able to have the commit message be something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;New Post: Great New Post https://ryancheley.com/yyyy/mm/dd/great-new-post/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is basically just three parts from the markdown file, the &lt;code&gt;Title&lt;/code&gt;, the &lt;code&gt;Date&lt;/code&gt;, and the &lt;code&gt;Slug&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In order to get those details, I need to review the structure of the markdown file. For Pelican writing in markdown my file is structured like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Slug&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Authors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;My&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;a href="https://www.ryancheley.com/2022/01/28/auto-generating-the-commit-message"&gt;the last post&lt;/a&gt; I wrote about how to &lt;code&gt;git add&lt;/code&gt; the files in the content directory. Here, I want to take the file that was added to &lt;code&gt;git&lt;/code&gt; and get the first 7 rows, i.e. the details from &lt;code&gt;Title&lt;/code&gt; to &lt;code&gt;Status&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The file that was updated that needs to be added to git can be identified by running&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find content -name '*.md' -print | sed 's/^/"/g' | sed 's/$/"/g' | xargs git add
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running &lt;code&gt;git status&lt;/code&gt; now will display which file was added with the last command and you'll see something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;
&lt;span class="nv"&gt;On&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;
&lt;span class="nv"&gt;Untracked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;:
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;committed&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;generating&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;.&lt;span class="nv"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What I need though is a more easily parsable output. Enter the &lt;code&gt;porcelin&lt;/code&gt; flag which, per the docs&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Give the output in an easy-to-parse format for scripts. This is similar to the short output, but will remain stable across Git versions and regardless of user configuration. See below for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;which is exactly what I needed.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;git status --porcelain&lt;/code&gt; you get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯ git status --porcelain
?? content/productivity/more-writing-automation.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, I just need to get the file path and exclude the status (the &lt;code&gt;??&lt;/code&gt; above in this case&lt;sup id="sf-auto-generating-the-commit-message-2-back"&gt;&lt;a href="#sf-auto-generating-the-commit-message-2" class="simple-footnote" title="Other values could just as easily be M or A"&gt;2&lt;/a&gt;&lt;/sup&gt;), which I can by piping in the results and using &lt;code&gt;sed&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯ git status --porcelain | sed s/^...//
content/productivity/more-writing-automation.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;sed&lt;/code&gt; portion says&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;search the output string starting at the beginning of the line (&lt;code&gt;^&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;find the first three characters (&lt;code&gt;...&lt;/code&gt;). &lt;sup id="sf-auto-generating-the-commit-message-3-back"&gt;&lt;a href="#sf-auto-generating-the-commit-message-3" class="simple-footnote" title="Why the first three characters, because that's how porcelain outputs the status"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;replace them with nothing (&lt;code&gt;//&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a couple of lines here that I need to get the content of for my commit message:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Slug&lt;/li&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Status&lt;sup id="sf-auto-generating-the-commit-message-4-back"&gt;&lt;a href="#sf-auto-generating-the-commit-message-4" class="simple-footnote" title="I will also need the Status to do some conditional logic otherwise I may have a post that is in draft status that I want to commit and the GitHub Action will run posting a tweet with an article and URL that don't actually exist yet. "&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I can use &lt;code&gt;head&lt;/code&gt; to get the first &lt;code&gt;n&lt;/code&gt; lines of a file. In this case, I need the first 7 lines of the output from &lt;code&gt;git status --porcelain | sed s/^...//&lt;/code&gt;. To do that, I pipe it to &lt;code&gt;head&lt;/code&gt;!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git status --porcelain | sed s/^...// | xargs head -7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That command will return this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Generating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;
&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Automation&lt;/span&gt;
&lt;span class="n"&gt;Slug&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;generating&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Deploying&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Words&lt;/span&gt;
&lt;span class="n"&gt;Authors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ryan&lt;/span&gt;
&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In order to get the &lt;strong&gt;Title&lt;/strong&gt;, I'll pipe this output to &lt;code&gt;grep&lt;/code&gt; to find the line with &lt;code&gt;Title&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git status --porcelain | sed s/^...// | xargs head -7 | grep 'Title: '
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which will return this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Generating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now I just need to remove the leading &lt;code&gt;Title:&lt;/code&gt; and I've got the title I'm going to need for my Commit message!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git status --porcelain | sed s/^...// | xargs head -7 | grep 'Title: ' | sed -e 's/Title: //g'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which return just&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Auto Generating the Commit Message
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I do this for each of the parts I need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Slug&lt;/li&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, this is getting to have a lot of parts, so I'm going to throw it into a &lt;code&gt;bash&lt;/code&gt; script file called &lt;code&gt;tweet.sh&lt;/code&gt;. The contents of the file look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;TITLE=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Title: ' | sed -e 's/Title: //g'`
SLUG=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Slug: ' | sed -e 's/Slug: //g'`
POST_DATE=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Date: ' | sed -e 's/Date: //g' | head -c 10 | grep '-' | sed -e 's/-/\//g'`
POST_STATUS=` git status --porcelain | sed s/^...// | xargs head -7 | grep 'Status: ' | sed -e 's/Status: //g'`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You'll see above that the &lt;code&gt;Date&lt;/code&gt; piece is a little more complicated, but it's just doing a find and replace on the &lt;code&gt;-&lt;/code&gt; to update them to &lt;code&gt;/&lt;/code&gt; for the URL.&lt;/p&gt;
&lt;p&gt;Now that I've got all of the pieces I need, it's time to start putting them together&lt;/p&gt;
&lt;p&gt;I define a new variable called &lt;code&gt;URL&lt;/code&gt; and set it&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;URL="https://ryancheley.com/$POST_DATE/$SLUG/"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and the commit message&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;MESSAGE="New Post: $TITLE $URL"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, all I need to do is wrap this in an &lt;code&gt;if&lt;/code&gt; statement so the command only runs when the STATUS is &lt;code&gt;published&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if [ $POST_STATUS = "published" ]
then
    MESSAGE="New Post: $TITLE $URL"

    git commit -m "$MESSAGE"

    git push github main
fi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Putting this all together (including the &lt;code&gt;git add&lt;/code&gt; from my previous post) and the &lt;code&gt;tweet.sh&lt;/code&gt; file looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Add the post to git
find content -name '*.md' -print | sed 's/^/"/g' | sed 's/$/"/g' | xargs git add


&lt;span class="gh"&gt;#&lt;/span&gt; Get the parts needed for the commit message
TITLE=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Title: ' | sed -e 's/Title: //g'`
SLUG=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Slug: ' | sed -e 's/Slug: //g'`
POST_DATE=`git status --porcelain | sed s/^...// | xargs head -7 | grep 'Date: ' | sed -e 's/Date: //g' | head -c 10 | grep '-' | sed -e 's/-/\//g'`
POST_STATUS=` git status --porcelain | sed s/^...// | xargs head -7 | grep 'Status: ' | sed -e 's/Status: //g'`

URL="https://ryancheley.com/$POST_DATE/$SLUG/"

if [ $POST_STATUS = "published" ]
then
    MESSAGE="New Post: $TITLE $URL"

    git commit -m "$MESSAGE"

    git push github main
fi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When this script is run it will find an updated or added markdown file (i.e. article) and add it to git. It will then parse the file to get data about the article. If the article is set to published it will commit the file with a message and will push to github. Once at GitHub, &lt;a href="https://www.ryancheley.com/2022/01/24/auto-tweeting-new-post/"&gt;the Tweeting action I wrote about&lt;/a&gt; will tweet my commit message!&lt;/p&gt;
&lt;p&gt;In the next (and last) article, I'm going to throw it all together and to get a spot when I can run one make command that will do all of this for me.&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;The script above works, but if you have multiple articles that you're working on at the same time, it will fail pretty spectacularly. The final version of the script has guards against that and looks like &lt;a href="https://github.com/ryancheley/ryancheley.com/blob/main/tweet.sh"&gt;this&lt;/a&gt;&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-auto-generating-the-commit-message-1"&gt;&lt;code&gt;make vercel&lt;/code&gt; actually runs &lt;code&gt;make html&lt;/code&gt; so this isn't really a step that I need to do. &lt;a href="#sf-auto-generating-the-commit-message-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-auto-generating-the-commit-message-2"&gt;Other values could just as easily be &lt;code&gt;M&lt;/code&gt; or &lt;code&gt;A&lt;/code&gt; &lt;a href="#sf-auto-generating-the-commit-message-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-auto-generating-the-commit-message-3"&gt;Why the first three characters, because that's how &lt;code&gt;porcelain&lt;/code&gt; outputs the &lt;code&gt;status&lt;/code&gt; &lt;a href="#sf-auto-generating-the-commit-message-3-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-auto-generating-the-commit-message-4"&gt;I will also need the &lt;code&gt;Status&lt;/code&gt; to do some conditional logic otherwise I may have a post that is in draft status that I want to commit and the GitHub Action will run posting a tweet with an article and URL that don't actually exist yet.
 &lt;a href="#sf-auto-generating-the-commit-message-4-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="productivity"></category><category term="Automation"></category></entry><entry><title>git add filename automation</title><link href="https://ryancheley.com/2022/01/26/git-add-filename-automation/" rel="alternate"></link><published>2022-01-26T00:00:00-08:00</published><updated>2022-01-26T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-01-26:/2022/01/26/git-add-filename-automation/</id><summary type="html">&lt;p&gt;In &lt;a href="https://www.ryancheley.com/2022/01/24/auto-tweeting-new-post/"&gt;my last post&lt;/a&gt; I mentioned the steps needed in order for me to post. They are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-git-add-filename-automation-1-back"&gt;&lt;a href="#sf-git-add-filename-automation-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/26/git-add-filename-automation/"&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to …&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;In &lt;a href="https://www.ryancheley.com/2022/01/24/auto-tweeting-new-post/"&gt;my last post&lt;/a&gt; I mentioned the steps needed in order for me to post. They are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-git-add-filename-automation-1-back"&gt;&lt;a href="#sf-git-add-filename-automation-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/26/git-add-filename-automation/"&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to be committed to GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git commit -m &amp;lt;message&amp;gt;&lt;/code&gt; to commit to GitHub&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ryancheley.com/2022/01/24/auto-tweeting-new-post/"&gt;Post to Twitter with a link to my new post&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In that post I focused on number 5, posting to Twitter with a link to the post using GitHub Actions.&lt;/p&gt;
&lt;p&gt;In this post I'll be focusing on how I automated step 3, "Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to be committed to GitHub".&lt;/p&gt;
&lt;h1&gt;Automating the &lt;code&gt;git add ...&lt;/code&gt; part of my workflow&lt;/h1&gt;
&lt;p&gt;I have my pelican content set up so that the category of a post is determined by the directory a markdown file is placed in. The structure of my content folder looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;content
├── musings
├── pages
├── productivity
├── professional\ development
└── technology
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you just just &lt;code&gt;git status&lt;/code&gt; on a directory it will give you the status of all of the files in that directory that have been changed, added, removed. Something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;
&lt;span class="nv"&gt;On&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;
&lt;span class="nv"&gt;Untracked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;:
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;committed&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;more&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;writing&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;automation&lt;/span&gt;.&lt;span class="nv"&gt;md&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;Makefile&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;metadata&lt;/span&gt;.&lt;span class="nv"&gt;json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That means that when you run &lt;code&gt;git add .&lt;/code&gt; all of those files will be added to git. For my purposes all that I need is the one updated file in the &lt;code&gt;content&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;The command &lt;code&gt;find&lt;/code&gt; does a great job of taking a directory and allowing you to search for what you want in that directory. You can run something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find content -name '*.md' -print
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And it will return essentially what you're looking for. Something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;curriculum&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;vitae&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;brag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;adding&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;omnifocus&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;making&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kd"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;how&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;learned&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;limiting&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;my&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;own&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;technical&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;debt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;just&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;happy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tweeting&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;my&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;outlook&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;outlook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;generating&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;productivity&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;declaring&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;omnifocus&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bankrupty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, because one of my categories has a space in it's name (&lt;code&gt;professional development&lt;/code&gt;) if you pipe the output of this to &lt;code&gt;xargs git add&lt;/code&gt; it fails with the error&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;fatal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pathspec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'content/professional'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;did&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In order to get around this, you need to surround the output of the results of &lt;code&gt;find&lt;/code&gt; with double quotes ("). You can do this by using &lt;code&gt;sed&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find content -name '*.md' -print | sed 's/^/"/g' | sed 's/$/"/g'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What this says is, take the output of &lt;code&gt;find&lt;/code&gt; and pipe it to &lt;code&gt;sed&lt;/code&gt; and use a global find and replace to add a &lt;code&gt;"&lt;/code&gt; to the start of the line (that's what the &lt;code&gt;^&lt;/code&gt; does) and then pipe that to &lt;code&gt;sed&lt;/code&gt; again and use a global find and replace to add a &lt;code&gt;"&lt;/code&gt; to the end of the line (that's what the '$' does).&lt;/p&gt;
&lt;p&gt;Now, when you run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find content -name '*.md' -print | sed 's/^/"/g' | sed 's/$/"/g'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The output looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s"&gt;"content/pages/404.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/pages/curriculum-vitae.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/pages/about.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/pages/brag.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/adding-the-new-file.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/omnifocus-3.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/making-the-right-choice-or-how-i-learned-to-live-with-limiting-my-own-technical-debt-and-just-be-happy.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/auto-tweeting-new-post.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/my-outlook-review-process.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/rules-and-actions-in-outlook.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/auto-generating-the-commit-message.md"&lt;/span&gt;
&lt;span class="s"&gt;"content/productivity/declaring-omnifocus-bankrupty.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, you can pipe your output to &lt;code&gt;xargs git add&lt;/code&gt; and there is no error!&lt;/p&gt;
&lt;p&gt;The final command looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find content -name '*.md' -print | sed 's/^/"/g' | sed 's/$/"/g' | xargs git add
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the next post, I'll walk through how I generate the commit message to be used in the automatic tweet!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-git-add-filename-automation-1"&gt;&lt;code&gt;make vercel&lt;/code&gt; actually runs &lt;code&gt;make html&lt;/code&gt; so this isn't really a step that I need to do. &lt;a href="#sf-git-add-filename-automation-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="productivity"></category></entry><entry><title>Auto Tweeting New Post</title><link href="https://ryancheley.com/2022/01/24/auto-tweeting-new-post/" rel="alternate"></link><published>2022-01-24T00:00:00-08:00</published><updated>2022-01-24T00:00:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-01-24:/2022/01/24/auto-tweeting-new-post/</id><summary type="html">&lt;p&gt;Each time I write something for this site there are several steps that I go through to make sure that the post makes it's way to where people can see it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-auto-tweeting-new-post-1-back"&gt;&lt;a href="#sf-auto-tweeting-new-post-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;Each time I write something for this site there are several steps that I go through to make sure that the post makes it's way to where people can see it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;make html&lt;/code&gt; to generate the SQLite database that powers my site's search tool&lt;sup id="sf-auto-tweeting-new-post-1-back"&gt;&lt;a href="#sf-auto-tweeting-new-post-1" class="simple-footnote" title="make vercel actually runs make html so this isn't really a step that I need to do."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make vercel&lt;/code&gt; to deploy the SQLite database to vercel&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; to add post to be committed to GitHub&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git commit -m &amp;lt;message&amp;gt;&lt;/code&gt; to commit to GitHub&lt;/li&gt;
&lt;li&gt;Post to Twitter with a link to my new post&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If there's more than 2 things to do, I'm totally going to forget to do one of them.&lt;/p&gt;
&lt;p&gt;The above steps are all automat-able, but the one I wanted to tackle first was the automated tweet. Last night I figured out how to tweet with a GitHub action.&lt;/p&gt;
&lt;p&gt;There were a few things to do to get the auto tweet to work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find a GitHub in the Market Place that did the auto tweet (or try to write one if I couldn't find one)&lt;/li&gt;
&lt;li&gt;Set up a twitter app with Read and Write privileges&lt;/li&gt;
&lt;li&gt;Set the necessary secrets for the report (API Key, API Key Secret, Access Token, Access Token Secret, Bearer)&lt;/li&gt;
&lt;li&gt;Test the GitHub Action&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The action I chose was &lt;a href="https://github.com/ethomson/send-tweet-action"&gt;send-tweet-action&lt;/a&gt;. It's got easy to read &lt;a href="https://github.com/ethomson/send-tweet-action/blob/main/README.md"&gt;documentation&lt;/a&gt; on what is needed. Honestly the hardest part was getting a twitter app set up with Read and Write privileges.&lt;/p&gt;
&lt;p&gt;I'm still not sure how to do it, honestly. I was lucky enough that I already had an app sitting around with Read and Write from the WordPress blog I had previously, so I just regenerated the keys for that one and used them.&lt;/p&gt;
&lt;p&gt;The last bit was just testing the action and seeing that it worked as expected. It was pretty cool running an action and then seeing a tweet in my timeline.&lt;/p&gt;
&lt;p&gt;The TIL for this was that GitHub Actions can have conditionals. This is important because I don't want to generate a new tweet each time I commit to main. I only want that to happen when I have a new post.&lt;/p&gt;
&lt;p&gt;To do that, you just need this in the GitHub Action:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    if: "contains(github.event.head_commit.message, '&amp;lt;String to Filter on&amp;gt;')"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In my case, the &lt;code&gt;&amp;lt;String to Filter on&amp;gt;&lt;/code&gt; is &lt;code&gt;New Post:&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;send-tweet-action&lt;/code&gt; has a &lt;code&gt;status&lt;/code&gt; field which is the text tweeted. I can use the &lt;code&gt;github.event.head_commit.message&lt;/code&gt; in the action like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_commit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="cp"&gt;}&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now when I have a commit message that starts 'New Post:' against &lt;code&gt;main&lt;/code&gt; I'll have a tweet get sent out too!&lt;/p&gt;
&lt;p&gt;This got me to thinking that I can/should automate all of these steps.&lt;/p&gt;
&lt;p&gt;With that in mind, I'm going to work on getting the process down to just having to run a single command. Something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    make publish "New Post: Title of my Post https://www.ryancheley.com/yyyy/mm/dd/slug/"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-auto-tweeting-new-post-1"&gt;&lt;code&gt;make vercel&lt;/code&gt; actually runs &lt;code&gt;make html&lt;/code&gt; so this isn't really a step that I need to do. &lt;a href="#sf-auto-tweeting-new-post-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="productivity"></category><category term='"GitHub Actions"'></category></entry><entry><title>Adding Search to My Pelican Blog with Datasette</title><link href="https://ryancheley.com/2022/01/16/adding-search-to-my-pelican-blog-with-datasette/" rel="alternate"></link><published>2022-01-16T19:30:00-08:00</published><updated>2022-01-16T19:30:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2022-01-16:/2022/01/16/adding-search-to-my-pelican-blog-with-datasette/</id><summary type="html">&lt;p&gt;Last summer I migrated my blog from &lt;a href="https://wordpress.com"&gt;Wordpress&lt;/a&gt; to &lt;a href="https://getpelican.com"&gt;Pelican&lt;/a&gt;. I did this for a couple of reasons (see my post &lt;a href="https://www.ryancheley.com/2021/07/02/migrating-to-pelican-from-wordpress/"&gt;here&lt;/a&gt;), but one thing that I was a bit worried about when I migrated was that Pelican's offering for site search didn't look promising.&lt;/p&gt;
&lt;p&gt;There was an outdated plugin …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last summer I migrated my blog from &lt;a href="https://wordpress.com"&gt;Wordpress&lt;/a&gt; to &lt;a href="https://getpelican.com"&gt;Pelican&lt;/a&gt;. I did this for a couple of reasons (see my post &lt;a href="https://www.ryancheley.com/2021/07/02/migrating-to-pelican-from-wordpress/"&gt;here&lt;/a&gt;), but one thing that I was a bit worried about when I migrated was that Pelican's offering for site search didn't look promising.&lt;/p&gt;
&lt;p&gt;There was an outdated plugin called &lt;a href="https://github.com/pelican-plugins/tipue-search"&gt;tipue-search&lt;/a&gt; but when I was looking at it I could tell it was on it's last legs.&lt;/p&gt;
&lt;p&gt;I thought about it, and since my blag isn't super high trafficked AND you can use google to search a specific site, I could wait a bit and see what options came up.&lt;/p&gt;
&lt;p&gt;After waiting a few months, I decided it would be interesting to see if I could write a SQLite utility to get the data from my blog, add it to a SQLite database and then use &lt;a href="https://datasette.io"&gt;datasette&lt;/a&gt; to serve it up.&lt;/p&gt;
&lt;p&gt;I wrote the beginning scaffolding for it last August in a utility called &lt;a href="https://pypi.org/project/pelican-to-sqlite/0.1/"&gt;pelican-to-sqlite&lt;/a&gt;, but I ran into several technical issues I just couldn't overcome. I thought about giving up, but sometimes you just need to take a step away from a thing, right?&lt;/p&gt;
&lt;p&gt;After the first of the year I decided to revisit my idea, but first looked to see if there was anything new for Pelican search. I found a tool plugin called &lt;a href="https://github.com/pelican-plugins/search"&gt;search&lt;/a&gt; that was released last November and is actively being developed, but as I read through the documentation there was just &lt;strong&gt;A LOT&lt;/strong&gt; of stuff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stork&lt;/li&gt;
&lt;li&gt;requirements for the structure of your page html&lt;/li&gt;
&lt;li&gt;static asset hosting&lt;/li&gt;
&lt;li&gt;deployment requires updating your &lt;code&gt;nginx&lt;/code&gt; settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These all looked a bit scary to me, and since I've done some work using &lt;a href="https://datasette.io"&gt;datasette&lt;/a&gt; I thought I'd revisit my initial idea.&lt;/p&gt;
&lt;h2&gt;My First Attempt&lt;/h2&gt;
&lt;p&gt;As I mentioned above, I wrote the beginning scaffolding late last summer. In my first attempt I tried to use a few tools to read the &lt;code&gt;md&lt;/code&gt; files and parse their &lt;code&gt;yaml&lt;/code&gt; structure and it just didn't work out. I also realized that &lt;code&gt;Pelican&lt;/code&gt; can have &lt;a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html"&gt;reStructured Text&lt;/a&gt; and that any attempt to parse just the &lt;code&gt;md&lt;/code&gt; file would never work for those file types.&lt;/p&gt;
&lt;h2&gt;My Second Attempt&lt;/h2&gt;
&lt;h3&gt;The Plugin&lt;/h3&gt;
&lt;p&gt;During the holiday I thought a bit about approaching the problem from a different perspective. My initial idea was to try and write a &lt;code&gt;datasette&lt;/code&gt; style package to read the data from &lt;code&gt;pelican&lt;/code&gt;. I decided instead to see if I could write a &lt;code&gt;pelican&lt;/code&gt; plugin to get the data and then add it to a SQLite database. It turns out, I can, and it's not that hard.&lt;/p&gt;
&lt;p&gt;Pelican uses &lt;code&gt;signals&lt;/code&gt; to make plugin in creation a pretty easy thing. I read a &lt;a href="https://blog.geographer.fr/pelican-plugins"&gt;post&lt;/a&gt; and the &lt;a href="https://docs.getpelican.com/en/latest/plugins.html"&gt;documentation&lt;/a&gt; and was able to start my effort to refactor &lt;code&gt;pelican-to-sqlite&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;From &lt;a href="https://blog.geographer.fr/pelican-plugins"&gt;The missing Pelican plugins guide&lt;/a&gt; I saw lots of different options, but realized that the signal &lt;code&gt;article_generator_write_article&lt;/code&gt; is what I needed to get the article content that I needed.&lt;/p&gt;
&lt;p&gt;I then also used &lt;code&gt;sqlite_utils&lt;/code&gt; to insert the data into a database table.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;save_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;table&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sqlite_utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;None&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;pragma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;slug&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;alter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Below is the method I wrote to take the content and turn it into a dictionary which can be used in the &lt;code&gt;save_items&lt;/code&gt; method above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;create_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;record &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;post_content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;html2text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html2text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;published_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%Y-%m-%d&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;html2text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html2text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://www.ryancheley.com/&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;published&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;record &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;author&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;category&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;post_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;published_date&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;published_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;slug&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;summary&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Putting these together I get a method used by the Pelican Plugin system that will generate the data I need for the site AND insert it into a SQLite database&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;create_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;save_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;article_generator_write_article&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The html template update&lt;/h3&gt;
&lt;p&gt;I use a custom implementation of &lt;a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/"&gt;Smashing Magazine&lt;/a&gt;. This allows me to do some edits, though I mostly keep it pretty stock. However, this allowed me to make a small edit to the &lt;code&gt;base.html&lt;/code&gt; template to include a search form.&lt;/p&gt;
&lt;p&gt;In order to add the search form I added the following code to &lt;code&gt;base.html&lt;/code&gt; below the &lt;code&gt;nav&lt;/code&gt; tag:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;relative h-8&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;absolute inset-y-0 right-10 w-128&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;class =&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;pl-4&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://search-ryancheley.vercel.app/pelican/article_search?text=name&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;get&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;site-search&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;site:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;search&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;site-search&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Search through site content&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;rounded-full w-16 hover:bg-blue-300&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Putting it all together with datasette and Vercel&lt;/h3&gt;
&lt;p&gt;Here's where the &lt;strong&gt;magic&lt;/strong&gt; starts. Publishing data to Vercel with &lt;code&gt;datasette&lt;/code&gt; is extremely easy with the &lt;code&gt;datasette&lt;/code&gt; plugin &lt;a href="https://pypi.org/project/datasette-publish-vercel/"&gt;&lt;code&gt;datasette-publish-vercel&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You do need to have the &lt;a href="https://vercel.com/cli"&gt;Vercel cli installed&lt;/a&gt;, but once you do, the steps for publishing your SQLite database is really well explained in the &lt;code&gt;datasette-publish-vercel&lt;/code&gt; &lt;a href="https://github.com/simonw/datasette-publish-vercel/blob/main/README.md"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One final step to do was to add a &lt;code&gt;MAKE&lt;/code&gt; command so I could just type a quick command which would create my content, generate the SQLite database AND publish the SQLite database to Vercel. I added the below to my &lt;code&gt;Makefile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;vercel&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;quot;Generate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;database&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;make&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;quot;Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;generation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;complete&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;quot;Publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;datasette&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pelican.db&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--project=search-ryancheley&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata.json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;quot;Publishing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;complete&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;datasette publish vercel pelican.db --project=search-ryancheley --metadata metadata.json; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;has an extra flag passed to it (&lt;code&gt;--metadata&lt;/code&gt;) which allows me to use &lt;code&gt;metadata.json&lt;/code&gt; to create a saved query which I call &lt;code&gt;article_search&lt;/code&gt;. The contents of that saved query are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;select summary as &amp;#39;Summary&amp;#39;, url as &amp;#39;URL&amp;#39;, published_date as &amp;#39;Published Data&amp;#39; from content where content like &amp;#39;%&amp;#39; || :text || &amp;#39;%&amp;#39; order by published_date
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is what allows the &lt;code&gt;action&lt;/code&gt; in the &lt;code&gt;form&lt;/code&gt; above to have a URL to link to in &lt;code&gt;datasette&lt;/code&gt; and return data!&lt;/p&gt;
&lt;p&gt;With just a few tweaks I'm able to include a search tool, powered by datasette for my pelican blog. Needless to say, I'm pretty pumped.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There are still a few things to do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;separate search form html file (for my site)&lt;/li&gt;
&lt;li&gt;formatting &lt;code&gt;datasette&lt;/code&gt; to match site (for my vercel powered instance of &lt;code&gt;datasette&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;update the README for &lt;code&gt;pelican-to-sqlite&lt;/code&gt; package to better explain how to fully implement&lt;/li&gt;
&lt;li&gt;Get &lt;code&gt;pelican-to-sqlite&lt;/code&gt; added to the &lt;a href="https://github.com/pelican-plugins/"&gt;pelican-plugins page&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content><category term="technology"></category><category term="Datasette"></category><category term="pelican"></category></entry><entry><title>The Well Maintained Test</title><link href="https://ryancheley.com/2021/11/22/the-well-maintained-test/" rel="alternate"></link><published>2021-11-22T19:57:00-08:00</published><updated>2021-11-22T19:57:00-08:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-11-22:/2021/11/22/the-well-maintained-test/</id><summary type="html">&lt;p&gt;At the beginning of November Adam Johnson &lt;a href="https://twitter.com/AdamChainz/status/1456347321415917569"&gt;tweeted&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’ve come up with a test that we can use to decide whether a new package we’re considering depending on is well-maintained.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and linked to an article he &lt;a href="https://adamj.eu/tech/2021/11/04/the-well-maintained-test/"&gt;wrote&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;He came up (&lt;a href="https://twitter.com/AdamChainz/status/1454041660879421442"&gt;with the help of Twitter&lt;/a&gt;) twelve questions to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At the beginning of November Adam Johnson &lt;a href="https://twitter.com/AdamChainz/status/1456347321415917569"&gt;tweeted&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’ve come up with a test that we can use to decide whether a new package we’re considering depending on is well-maintained.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and linked to an article he &lt;a href="https://adamj.eu/tech/2021/11/04/the-well-maintained-test/"&gt;wrote&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;He came up (&lt;a href="https://twitter.com/AdamChainz/status/1454041660879421442"&gt;with the help of Twitter&lt;/a&gt;) twelve questions to ask of any library that you're looking at:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is it described as “production ready”?&lt;/li&gt;
&lt;li&gt;Is there sufficient documentation?&lt;/li&gt;
&lt;li&gt;Is there a changelog?&lt;/li&gt;
&lt;li&gt;Is someone responding to bug reports?&lt;/li&gt;
&lt;li&gt;Are there sufficient tests?&lt;/li&gt;
&lt;li&gt;Are the tests running with the latest &amp;lt;Language&amp;gt; version?&lt;/li&gt;
&lt;li&gt;Are the tests running with the latest &amp;lt;Integration&amp;gt; version?&lt;/li&gt;
&lt;li&gt;Is there a Continuous Integration (CI) configuration?&lt;/li&gt;
&lt;li&gt;Is the CI passing?&lt;/li&gt;
&lt;li&gt;Does it seem relatively well used?&lt;/li&gt;
&lt;li&gt;Has there been a commit in the last year?&lt;/li&gt;
&lt;li&gt;Has there been a release in the last year?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I thought it would be interesting to turn that checklist into a Click App using &lt;a href="https://github.com/simonw/click-app"&gt;Simon Willison's Click App Cookiecutter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I set out in earnest to do just that on &lt;a href="https://github.com/ryancheley/the-well-maintained-test/commit/94e8028e4d3a817ab0b26168b4285231de6f141c"&gt;November 8th&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What started out as just a simple Click app, quickly turned in a pretty robust CLI using &lt;a href="https://twitter.com/willmcgugan"&gt;Will McGugan&lt;/a&gt;'s &lt;a href="https://github.com/willmcgugan/rich"&gt;Rich&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;I started by using the GitHub API to try and answer the questions, but quickly found that it couldn't answer them all. Then I cam across the PyPI API which helped to answer almost all of them programmatically.&lt;/p&gt;
&lt;p&gt;There's still a &lt;a href="https://github.com/ryancheley/the-well-maintained-test/issues"&gt;bit of work&lt;/a&gt; to do to get it where I want it to, but it's pretty sweet that I can now run a simple command and review the output to see if the package is well maintained.&lt;/p&gt;
&lt;p&gt;You can even try it on the package I wrote!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;the-well-maintained-test https://github.com/ryancheley/the-well-maintained-test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which will return (as of this writing) the output below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;described&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;read&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Development&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;
&lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sufficient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;documentation&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Documentation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;
&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;well&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;maintained&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kr"&gt;READ&lt;/span&gt;&lt;span class="n"&gt;ME&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;
&lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;changelog&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;
&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;someone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;responding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maintainer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;respond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sin&lt;/span&gt;&lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;made&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bug&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
&lt;span class="mf"&gt;5.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sufficient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;
&lt;span class="mf"&gt;6.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="n"&gt;ning&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Language&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;supports&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;following&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;programming&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;languages&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.7&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.8&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.9&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;

&lt;span class="mf"&gt;7.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;run&lt;/span&gt;&lt;span class="n"&gt;ning&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="n"&gt;egration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;associated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frameworks&lt;/span&gt;
&lt;span class="mf"&gt;8.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;Cont&lt;/span&gt;&lt;span class="n"&gt;inuous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="n"&gt;egration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;workflows&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;

&lt;span class="mf"&gt;9.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passing&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;
&lt;span class="mf"&gt;10.&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;relatively&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;well&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;following&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Watchers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;For&lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;Open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subscribers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="mf"&gt;11.&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2021&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ago&lt;/span&gt;
&lt;span class="mf"&gt;12.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Yes&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2021&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ago&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There is still one question that I haven't been able to answer programmatically  with an API and that is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Are there sufficient tests?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When that question comes up, you're prompted in the terminal to answer either &lt;code&gt;y/n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But, it does leave room for a fix by someone else!&lt;/p&gt;</content><category term="technology"></category><category term="Python"></category><category term="python package"></category></entry><entry><title>Styling Clean Up with Bash</title><link href="https://ryancheley.com/2021/10/26/styling-cleanup/" rel="alternate"></link><published>2021-10-26T19:23:00-07:00</published><updated>2021-10-26T19:23:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-10-26:/2021/10/26/styling-cleanup/</id><summary type="html">&lt;p&gt;I have a side project I've been working on for a while now. One thing that happened overtime is that the styling of the site grew organically. I'm not a designer, and I didn't have a master set of templates or design principals guiding the development. I kind of hacked …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have a side project I've been working on for a while now. One thing that happened overtime is that the styling of the site grew organically. I'm not a designer, and I didn't have a master set of templates or design principals guiding the development. I kind of hacked it together and made it look "nice enough"&lt;/p&gt;
&lt;p&gt;That was until I really starting going from one page to another and realized that there styling of various pages wasn't just a little off ... but A LOT off.&lt;/p&gt;
&lt;p&gt;As an aside, I'm using &lt;a href="https://www.tailwind.com"&gt;tailwind&lt;/a&gt; as my CSS Framework&lt;/p&gt;
&lt;p&gt;I wanted to make some changes to the styling and realized I had two choices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Manually go through each html template (the project is a Django project) and catalog the styles used for each element&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OR&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try and write a &lt;code&gt;bash&lt;/code&gt; command to do it for me&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Well, before we jump into either choice, let's see how many templates there are to review!&lt;/p&gt;
&lt;p&gt;As I said above, this is a Django project. I keep all of my templates in a single &lt;code&gt;templates&lt;/code&gt; directory with each app having it's own sub directory.&lt;/p&gt;
&lt;p&gt;I was able to use this one line to count the number of &lt;code&gt;html&lt;/code&gt; files in the templates directory (and all of the sub directories as well)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls -R templates | grep html | wc -l
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are 3 parts to this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ls -R templates&lt;/code&gt; will list out all of the files recursively list subdirectories encountered in the templates directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep html&lt;/code&gt; will make sure to only return those files with &lt;code&gt;html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -l&lt;/code&gt; uses the word, line, character, and byte count to return the number of lines return from the previous command&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In each case one command is piped to the next.&lt;/p&gt;
&lt;p&gt;This resulted in 41 &lt;code&gt;html&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;OK, I'm not going to want to manually review 41 files. Looks like we'll be going with option 2, "Try and write a &lt;code&gt;bash&lt;/code&gt; command to do it for me"&lt;/p&gt;
&lt;p&gt;In the end the &lt;code&gt;bash&lt;/code&gt; script is actually relatively straight forward. We're just using &lt;code&gt;grep&lt;/code&gt; two times. But it's the options on &lt;code&gt;grep&lt;/code&gt; that change (as well as the regex used) that are what make the magic happen&lt;/p&gt;
&lt;p&gt;The first thing I want to do is find all of the lines that have the string &lt;code&gt;class=&lt;/code&gt; in them. Since there are &lt;code&gt;html&lt;/code&gt; templates, that's a pretty sure fire way to find all of the places where the styles I am interested in are being applied&lt;/p&gt;
&lt;p&gt;I use a package called &lt;code&gt;djhtml&lt;/code&gt; to lint my templates, but just in case something got missed, I want to ignore case when doing my regex, i.e, &lt;code&gt;class=&lt;/code&gt; should be found, but so should &lt;code&gt;cLass=&lt;/code&gt; or &lt;code&gt;Class=&lt;/code&gt;. In order to get that I need to have the &lt;code&gt;i&lt;/code&gt; flag enabled.&lt;/p&gt;
&lt;p&gt;Since the &lt;code&gt;html&lt;/code&gt; files may be in the base directory &lt;code&gt;templates&lt;/code&gt; or one of the subdirectories, I need to recursively search, so I include the &lt;code&gt;r&lt;/code&gt; flag as well&lt;/p&gt;
&lt;p&gt;This gets us&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grep -ri &amp;quot;class=&amp;quot; templates/*
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That command will output a whole lines like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;templates/tasks/steps_lists.html:&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;table-fixed w-full border text-center&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;w-1/2 flex justify-left-2 p-2&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Task&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;w-1/4 justify-center p-2&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;w-1/4 justify-center p-2&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;flex justify-left-2 p-2&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;p-2 text-center&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;block hover:text-gray-600&amp;quot;&lt;/span&gt;
&lt;span class="err"&gt;templates/tasks/steps_lists.html:&lt;/span&gt;&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fas fa-edit&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/i&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;p-2 text-center&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
templates/tasks/steps_lists.html:&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;block hover:text-gray-600&amp;quot;&lt;/span&gt;
&lt;span class="err"&gt;templates/tasks/steps_lists.html:&lt;/span&gt;&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fas fa-trash-alt&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/i&amp;gt;&lt;/span&gt;
templates/tasks/step_form.html:&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bg-gray-400 text-center py-2&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
templates/tasks/step_form.html:&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;submit&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;view.action&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="s2"&gt;:&amp;quot;Add&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great! We have the data we need, now we just want to clean it up.&lt;/p&gt;
&lt;p&gt;Again, we'll use &lt;code&gt;grep&lt;/code&gt; only this time we want to look for an honest to goodness regular expression. We're trying to identify everything in between the first open angle brackey (&amp;lt;) and the first closed angle bracket (&amp;gt;)&lt;/p&gt;
&lt;p&gt;A bit of googling, searching stack overflow, and playing with the great site &lt;a href="https://regex101.com"&gt;regex101.com&lt;/a&gt; gets you this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;lt;[^\/].*?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OK, we have the regular expression we need, but what options do we need to use in &lt;code&gt;grep&lt;/code&gt;? In this case we actually have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;egrep&lt;/code&gt; (which allows for extended regular expressions)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;grep -E&lt;/code&gt; to make grep behave like &lt;code&gt;egrep&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I chose to go with option 2, use &lt;code&gt;grep -E&lt;/code&gt;. Next, we want to return ONLY the part of the line that matches the regex. For that, we can use the option &lt;code&gt;o&lt;/code&gt;. Putting it all together we get&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grep -Eo &amp;quot;&amp;lt;[^\/].*?&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, we can pipe the results from our first command into our second command and we get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grep -ri &amp;quot;class=&amp;quot; templates/* | grep -Eo &amp;quot;&amp;lt;[^\/].*?&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will output to standard out, but next I really want to use a tool for aggregation and comparison. It was at this point that I decided the best next tool to use would be Excel. So I sent the output to a text file and then opened that text file in Excel to do the final review. To output the above to a text file called &lt;code&gt;tailwind.txt&lt;/code&gt; we&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grep -ri &amp;quot;class=&amp;quot; templates/* | grep -Eo &amp;quot;&amp;lt;[^\/].*?&amp;gt;&amp;quot; &amp;gt; tailwind.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With these results I was able to find several styling inconsistencies and then fix them up. In all it took me a few nights of working out the bash commands and then a few more nights to get the styling consistent. In the process I learned &lt;strong&gt;so&lt;/strong&gt; much about &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;egrep&lt;/code&gt;. It was a good exercise to have gone through.&lt;/p&gt;</content><category term="technology"></category><category term="css"></category><category term="tailwind"></category><category term="bash"></category></entry><entry><title>djhtml and justfile</title><link href="https://ryancheley.com/2021/08/22/djhtml-and-justfile/" rel="alternate"></link><published>2021-08-22T00:00:00-07:00</published><updated>2021-08-22T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-08-22:/2021/08/22/djhtml-and-justfile/</id><summary type="html">&lt;p&gt;I had read about a project called djhtml and wanted to use it on one of my projects. The documentation is really good for adding it to precommit-ci, but I wasn't sure what I needed to do to just run it on the command line.&lt;/p&gt;
&lt;p&gt;It took a bit of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I had read about a project called djhtml and wanted to use it on one of my projects. The documentation is really good for adding it to precommit-ci, but I wasn't sure what I needed to do to just run it on the command line.&lt;/p&gt;
&lt;p&gt;It took a bit of googling, but I was finally able to get the right incantation of commands to be able to get it to run on my templates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;djhtml -i $(find templates -name &amp;#39;*.html&amp;#39; -print)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But of course because I have the memory of a goldfish and this is more than 3 commands to try to remember to string together, instead of telling myself I would remember it, I simply added it to a just file and now have this recipe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; applies djhtml linting to templates
djhtml:
    djhtml -i $(find templates -name &amp;#39;*.html&amp;#39; -print)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This means that I can now run &lt;code&gt;just djhtml&lt;/code&gt; and I can apply djhtml's linting to my templates.&lt;/p&gt;
&lt;p&gt;Pretty darn cool if you ask me. But then I got to thinking, I can make this a bit more general for 'linting' type activities. I include all of these in my precommit-ci, but I figured, what the heck, might as well have a just recipe for all of them!&lt;/p&gt;
&lt;p&gt;So I refactored the recipe to be this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; applies linting to project (black, djhtml, flake8)
lint:
    djhtml -i $(find templates -name &amp;#39;*.html&amp;#39; -print)
    black .
    flake8 .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And now I can run all of these linting style libraries with a single command &lt;code&gt;just lint&lt;/code&gt;&lt;/p&gt;</content><category term="technology"></category><category term="django"></category><category term="djhtml"></category><category term="just"></category></entry><entry><title>Prototyping with Datasette</title><link href="https://ryancheley.com/2021/08/09/prototyping-with-datasette/" rel="alternate"></link><published>2021-08-09T18:26:00-07:00</published><updated>2021-08-09T18:26:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-08-09:/2021/08/09/prototyping-with-datasette/</id><summary type="html">&lt;p&gt;At my job I work with some really talented Web Developers that are saddled with a pretty creaky legacy system.&lt;/p&gt;
&lt;p&gt;We're getting ready to start on a new(ish) project where we'll be taking an old project built on this creaky legacy system (&lt;code&gt;VB.net&lt;/code&gt;) and re-implementing it on a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At my job I work with some really talented Web Developers that are saddled with a pretty creaky legacy system.&lt;/p&gt;
&lt;p&gt;We're getting ready to start on a new(ish) project where we'll be taking an old project built on this creaky legacy system (&lt;code&gt;VB.net&lt;/code&gt;) and re-implementing it on a &lt;code&gt;C#&lt;/code&gt; backend and an &lt;code&gt;Angular&lt;/code&gt; front end. We'll be working on a lot of new features and integrations so it's worth rebuilding it versus shoehorning the new requirements into the legacy system.&lt;/p&gt;
&lt;p&gt;The details of the project aren't really important. What is important is that as I was reviewing the requirements with the Web Developer Supervisor he said something to the effect of, "We can create a proof of concept and just hard code the data in a json file to fake th backend."&lt;/p&gt;
&lt;p&gt;The issue is ... we already have the data that we'll need in a MS SQL database (it's what is running the legacy version) it's just a matter of getting it into the right json "shape".&lt;/p&gt;
&lt;p&gt;Creating a 'fake' json object that kind of/maybe mimics the real data is something we've done before, and it ALWAYS seems to bite us in the butt. We don't account for proper pagination, or the real lengths of data in the fields or NULL values or whatever shenanigans happen to befall real world data!&lt;/p&gt;
&lt;p&gt;This got me thinking about &lt;a href="https://simonwillison.net"&gt;Simon Willison&lt;/a&gt;'s project &lt;a href="https://datasette.io"&gt;Datasette&lt;/a&gt; and using it to prototype the API end points we would need.&lt;/p&gt;
&lt;p&gt;I had been trying to figure out how to use the &lt;code&gt;db-to-sqlite&lt;/code&gt; to extract data from a MS SQL database into a SQLite database and was successful (see my PR to &lt;code&gt;db-to-sqlite&lt;/code&gt; &lt;a href="https://github.com/ryancheley/db-to-sqlite/tree/ryancheley-patch-1-document-updates#using-db-to-sqlite-with-ms-sql"&gt;here&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;With this idea in hand, I reviewed it with the Supervisor and then scheduled a call with the web developers to review &lt;code&gt;datasette&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;During this meeting, I wanted to review:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The motivation behind why we would want to use it&lt;/li&gt;
&lt;li&gt;How we could leverage it to do &lt;a href="https://datasette.io/for/rapid-prototyping"&gt;Rapid Prototyping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Give a quick demo data from the stored procedure that did the current data return for the legacy project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In all it took less than 10 minutes to go from nothing to a local instance of &lt;code&gt;datasette&lt;/code&gt; running with a prototype JSON API for the web developers to see.&lt;/p&gt;
&lt;p&gt;I'm hoping to see the Web team use this concept more going forward as I can see huge benefits for Rapid Prototyping of ideas, especially if you already have the data housed in a database. But even if you don't, &lt;code&gt;datasette&lt;/code&gt; has tons of &lt;a href="https://datasette.io/tools"&gt;tools&lt;/a&gt; to get the data from a variety of sources into a SQLite database to use and then you can do the rapid prototyping!&lt;/p&gt;</content><category term="technology"></category><category term="Datasette"></category></entry><entry><title>Contributing to Tryceratops</title><link href="https://ryancheley.com/2021/08/07/contributing-to-tryceratops/" rel="alternate"></link><published>2021-08-07T00:00:00-07:00</published><updated>2021-08-07T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-08-07:/2021/08/07/contributing-to-tryceratops/</id><summary type="html">&lt;p&gt;I read about a project called &lt;a href="https://pypi.org/project/tryceratops/"&gt;Tryceratops&lt;/a&gt; on Twitter when it was &lt;a href="https://twitter.com/webology/status/1414233648534933509"&gt;tweeted about by Jeff Triplet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I checked it out and it seemed interesting. I decided to use it on my &lt;a href="https://doestatisjrhaveanerrortoday.com"&gt;simplest Django project&lt;/a&gt; just to give it a test drive running this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tryceratops .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and got this result …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I read about a project called &lt;a href="https://pypi.org/project/tryceratops/"&gt;Tryceratops&lt;/a&gt; on Twitter when it was &lt;a href="https://twitter.com/webology/status/1414233648534933509"&gt;tweeted about by Jeff Triplet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I checked it out and it seemed interesting. I decided to use it on my &lt;a href="https://doestatisjrhaveanerrortoday.com"&gt;simplest Django project&lt;/a&gt; just to give it a test drive running this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tryceratops .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and got this result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Done processing! 🦖✨
Processed 16 files
Found 0 violations
Failed to process 1 files
Skipped 2340 files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is nice, but what is the file that failed to process?&lt;/p&gt;
&lt;p&gt;This left me with two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Complain that this awesome tool created by someone didn't do the thing I thought it needed to do&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OR&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Submit an issue to the project and offer to help.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went with option 2 😀&lt;/p&gt;
&lt;p&gt;My initial commit was made in a pretty naive way. It did the job, but not in the best way for maintainability. I had a really great exchange with the maintainer &lt;a href="https://github.com/guilatrova"&gt;Guilherme Latrova&lt;/a&gt; about the change that was made and he helped to direct me in a different direction.&lt;/p&gt;
&lt;p&gt;The biggest thing I learned while working on this project (for Python at least) was the &lt;code&gt;logging&lt;/code&gt; library. Specifically I learned how to add:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a formatter&lt;/li&gt;
&lt;li&gt;a handler&lt;/li&gt;
&lt;li&gt;a logger&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my change, I added a simple format with a verbose handler in a custom logger. It looked something like this:&lt;/p&gt;
&lt;p&gt;The formatter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s"&gt;&amp;quot;simple&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;format&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%(message)s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The handler:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s"&gt;&amp;quot;verbose_output&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;class&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;logging.StreamHandler&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;level&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;DEBUG&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;formatter&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;simple&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stream&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;ext://sys.stdout&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The logger:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;quot;loggers&amp;quot;: {
    &amp;quot;tryceratops&amp;quot;: {
        &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,
        &amp;quot;handlers&amp;quot;: [
            &amp;quot;verbose_output&amp;quot;,
        ],
    },
},
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This allows the &lt;code&gt;verbose&lt;/code&gt; flag to output the message to Standard Out and give and &lt;code&gt;INFO&lt;/code&gt; level of detail.&lt;/p&gt;
&lt;p&gt;Because of what I learned, I've started using the &lt;a href="https://docs.python.org/3/library/logging.html"&gt;logging library&lt;/a&gt; on some of my work projects where I had tried to roll my own logging tool. I should have known there was a logging tool in the Standard Library BEFORE I tried to roll me own 🤦🏻‍♂️&lt;/p&gt;
&lt;p&gt;The other thing I (kind of) learned how to do was to squash my commits. I had never had a need (or desire?) to squash commits before, but the commit message is what Guilherme uses to generate the change log. So, with his guidance and help I tried my best to squash those commits. Although in the end he had to do it (still not entiredly sure what I did wrong) I was exposed to the idea of squashing commits and why they might be done. A win-win!&lt;/p&gt;
&lt;p&gt;The best part about this entire experience was getting to work with Guilherme Latrova. He was super helpful and patient and had great advice without telling me what to do. The more I work within the Python ecosystem the more I'm just blown away by just how friendly and helpful everyone is and it's what make me want to do these kinds of projects.&lt;/p&gt;
&lt;p&gt;If you haven't had a chance to work on an open source project, I highly recommend it. It's a great chance to learn and to meet new people.&lt;/p&gt;</content><category term="technology"></category><category term="oss"></category><category term="contributing"></category></entry><entry><title>Contributing to django-sql-dashboard</title><link href="https://ryancheley.com/2021/07/09/contributing-to-django-sql-dashboard/" rel="alternate"></link><published>2021-07-09T00:00:00-07:00</published><updated>2021-07-09T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-07-09:/2021/07/09/contributing-to-django-sql-dashboard/</id><summary type="html">&lt;p&gt;Last Saturday (July 3rd) while on vacation, I dubbed it “Security update Saturday”. I took the opportunity to review all of the GitHub bot alerts about out of date packages, and make the updates I needed to.&lt;/p&gt;
&lt;p&gt;This included updated &lt;code&gt;django-sql-dashboard&lt;/code&gt; to &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/1.0"&gt;version 1.0&lt;/a&gt; … which I was really excited …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last Saturday (July 3rd) while on vacation, I dubbed it “Security update Saturday”. I took the opportunity to review all of the GitHub bot alerts about out of date packages, and make the updates I needed to.&lt;/p&gt;
&lt;p&gt;This included updated &lt;code&gt;django-sql-dashboard&lt;/code&gt; to &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/1.0"&gt;version 1.0&lt;/a&gt; … which I was really excited about doing. It included two things I was eager to see:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Implemented a new column cog menu, with options for sorting, counting distinct items and counting by values. &lt;a href="https://github.com/simonw/django-sql-dashboard/issues/57"&gt;#57&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Admin change list view now only shows dashboards the user has permission to edit. Thanks, &lt;a href="https://github.com/atverma"&gt;Atul Varma&lt;/a&gt;. &lt;a href="https://github.com/simonw/django-sql-dashboard/issues/130"&gt;#130&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I made the updates on my site StadiaTracker.com using my normal workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make the change locally on my MacBook Pro&lt;/li&gt;
&lt;li&gt;Run the tests&lt;/li&gt;
&lt;li&gt;Push to UAT&lt;/li&gt;
&lt;li&gt;Push to PROD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The next day, on July 4th, I got the following error message via my error logging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;Internal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;games&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;

&lt;span class="nx"&gt;ProgrammingError&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;games&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="nx"&gt;could&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql_identifier&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So I copied the &lt;a href="https://stadiatracker.com/dashboard/games-seen-in-person/"&gt;url&lt;/a&gt; &lt;code&gt;/dashboard/games-seen-in-person/&lt;/code&gt; to see if I could replicate the issue as an authenticated user and sure enough, I got a 500 Server error.&lt;/p&gt;
&lt;h2&gt;Troubleshooting process&lt;/h2&gt;
&lt;p&gt;The first thing I did was to fire up the local version and check the url there. Oddly enough, it worked without issue.&lt;/p&gt;
&lt;p&gt;OK … well that’s odd. What are the differences between the local version and the uat / prod version?&lt;/p&gt;
&lt;p&gt;The local version is running on macOS 10.15.7 while the uat / prod versions are running Ubuntu 18.04. That could be one source of the issue.&lt;/p&gt;
&lt;p&gt;The local version is running Postgres 13.2 while the uat / prod versions are running Postgres 10.17&lt;/p&gt;
&lt;p&gt;OK, two differences. Since the error is &lt;code&gt;could not find array type for data type information_schema.sql_identifier&lt;/code&gt; I’m going to start with taking a look at the differences on the Postgres versions.&lt;/p&gt;
&lt;p&gt;First, I looked at the &lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;Change Log&lt;/a&gt; to see what changed between version 0.16 and version 1.0. Nothing jumped out at me, so I looked at the &lt;a href="https://github.com/simonw/django-sql-dashboard/compare/acb3752..b8835"&gt;diff&lt;/a&gt; between several files between the two versions looking specifically for &lt;code&gt;information_schema.sql_identifier&lt;/code&gt; which didn’t bring up anything.&lt;/p&gt;
&lt;p&gt;Next I checked for either &lt;code&gt;information_schema&lt;/code&gt; or &lt;code&gt;sql_identifier&lt;/code&gt; and found a chance in the &lt;code&gt;views.py&lt;/code&gt; file. On line 151 (version 0.16) this change was made:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;string_agg(column_name, &amp;#39;, &amp;#39; order by ordinal_position) as columns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;array_to_json(array_agg(column_name order by ordinal_position)) as columns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, I extracted the entire SQL statement from the &lt;code&gt;views.py&lt;/code&gt; file to run in Postgres on the UAT server&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;            with visible_tables as (
              select table_name
                from information_schema.tables
                where table_schema = &amp;#39;public&amp;#39;
                order by table_name
            ),
            reserved_keywords as (
              select word
                from pg_get_keywords()
                where catcode = &amp;#39;R&amp;#39;
            )
            select
              information_schema.columns.table_name,
              array_to_json(array_agg(column_name order by ordinal_position)) as columns
            from
              information_schema.columns
            join
              visible_tables on
              information_schema.columns.table_name = visible_tables.table_name
            where
              information_schema.columns.table_schema = &amp;#39;public&amp;#39;
            group by
              information_schema.columns.table_name
            order by
              information_schema.columns.table_name
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running this generated the same error I was seeing from the logs!&lt;/p&gt;
&lt;p&gt;Next, I picked apart the various select statements, testing each one to see what failed, and ended on this one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;select information_schema.columns.table_name,
array_to_json(array_agg(column_name order by ordinal_position)) as columns
from information_schema.columns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which generated the same error message. Great!&lt;/p&gt;
&lt;p&gt;In order to determine how to proceed next I googled &lt;code&gt;sql_identifier&lt;/code&gt; to see what it was. Turns out it’s a field type in Postgres! (I’ve been working in MSSQL for more than 10 years and as far as I know, this isn’t a field type over there, so I learned something)&lt;/p&gt;
&lt;p&gt;Further, there were &lt;a href="https://bucardo.org/postgres_all_versions#version_12.0"&gt;changes made to that field type in Postgres 12&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;OK, since there were changes made to that afield type in Postgres 12, I’ll probably need to cast the field to another field type that won’t fail.&lt;/p&gt;
&lt;p&gt;That led me to try this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;select information_schema.columns.table_name,
array_to_json(array_agg(cast(column_name as text) order by ordinal_position)) as columns
from information_schema.columns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which returned a value without error!&lt;/p&gt;
&lt;h2&gt;Submitting the updated code&lt;/h2&gt;
&lt;p&gt;With the solution in hand, I read the &lt;a href="https://github.com/simonw/django-sql-dashboard/blob/main/docs/contributing.md"&gt;Contribution Guide&lt;/a&gt; and submitting my patch. And the most awesome part? Within less than an hour Simon Willison (the project’s maintainer) had replied back and merged by code!&lt;/p&gt;
&lt;p&gt;And then, the icing on the cake was getting a &lt;a href="https://simonwillison.net/2021/Jul/6/django-sql-dashboard/"&gt;shout out in a post that Simon wrote&lt;/a&gt; up about the update that I submitted!&lt;/p&gt;
&lt;p&gt;Holy smokes that was sooo cool.&lt;/p&gt;
&lt;p&gt;I love solving problems, and I love writing code, so this kind of stuff just really makes my day.&lt;/p&gt;
&lt;p&gt;Now, I’ve contributed to an open source project (that makes 3 now!) and the issue with the &lt;code&gt;/dashboard/&lt;/code&gt; has been fixed.&lt;/p&gt;
&lt;p&gt;All&lt;/p&gt;</content><category term="technology"></category><category term="oss"></category><category term="contributing"></category><category term="Django"></category></entry><entry><title>Publishing content to Pelican site</title><link href="https://ryancheley.com/2021/07/07/publishing-content-to-pelican-site/" rel="alternate"></link><published>2021-07-07T00:00:00-07:00</published><updated>2021-07-07T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-07-07:/2021/07/07/publishing-content-to-pelican-site/</id><summary type="html">&lt;p&gt;There are a lot of different ways to get the content for your Pelican site onto the internet. The &lt;a href="https://docs.getpelican.com/en/latest/publish.html"&gt;Docs show&lt;/a&gt; an example using &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For automation they talk about the use of either &lt;code&gt;Invoke&lt;/code&gt; or &lt;code&gt;Make&lt;/code&gt; (although you could also use &lt;a href="https://github.com/casey/just"&gt;&lt;code&gt;Just&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;Make&lt;/code&gt; which is my preferred …&lt;/p&gt;</summary><content type="html">&lt;p&gt;There are a lot of different ways to get the content for your Pelican site onto the internet. The &lt;a href="https://docs.getpelican.com/en/latest/publish.html"&gt;Docs show&lt;/a&gt; an example using &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For automation they talk about the use of either &lt;code&gt;Invoke&lt;/code&gt; or &lt;code&gt;Make&lt;/code&gt; (although you could also use &lt;a href="https://github.com/casey/just"&gt;&lt;code&gt;Just&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;Make&lt;/code&gt; which is my preferred command runner.)&lt;/p&gt;
&lt;p&gt;I didn't go with any of these options, instead opting to use GitHub Actions instead.&lt;/p&gt;
&lt;p&gt;I have &lt;a href="https://github.com/ryancheley/ryancheley.com/tree/main/.github/workflows"&gt;two GitHub Actions&lt;/a&gt; that will publish updated content. One action publishes to a UAT version of the site, and the other to the Production version of the site.&lt;/p&gt;
&lt;p&gt;Why two actions you might ask?&lt;/p&gt;
&lt;p&gt;Right now it's so that I can work through making my own theme and deploying it without disrupting the content on my production site. Also, it's a workflow that I'm pretty used to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Local Development&lt;/li&gt;
&lt;li&gt;Push to Development Branch on GitHub&lt;/li&gt;
&lt;li&gt;Pull Request into Main on GitHub&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It kind of complicates things right now, but I feel waaay more comfortable with having a UAT version of my site that I can just undo if I need to.&lt;/p&gt;
&lt;p&gt;Below is the code for the &lt;a href="https://raw.githubusercontent.com/ryancheley/ryancheley.com/main/.github/workflows/publish.yml"&gt;Prod Deployment&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Pelican&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;

&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;18.04&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;appleboy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSH_HOST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSH_KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSH_USERNAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;git&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/home/ryancheley/venv/bin/&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="sr"&gt;/* /home/ryancheley/&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/home/&lt;/span&gt;&lt;span class="n"&gt;ryancheley&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;txt&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;pelican&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;publishconf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's break it down a bit&lt;/p&gt;
&lt;p&gt;Lines 3 - 6 are just indicating when the actually perform the actions in the lines below.&lt;/p&gt;
&lt;p&gt;In line 13 I invoke the &lt;code&gt;appleboy/ssh-action@v0.1.2&lt;/code&gt; which allows me to ssh into my server and then run some command line functions.&lt;/p&gt;
&lt;p&gt;On line 20 I remove the folder where the code was previously cloned from, and in line 21 I run the &lt;code&gt;git clone&lt;/code&gt; command to download the code&lt;/p&gt;
&lt;p&gt;Line 23 I activate my virtual environment&lt;/p&gt;
&lt;p&gt;Line 25 I copy the code from the cloned repo into the directory of my site&lt;/p&gt;
&lt;p&gt;Line 27 I change directory into the source for the site&lt;/p&gt;
&lt;p&gt;Line 29 I make any updates to requirements with &lt;code&gt;pip install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Finally, in line 31 I run the command to publish the content (which takes my &lt;code&gt;.md&lt;/code&gt; files and turns them into HTML files to be seen on the internet)&lt;/p&gt;</content><category term="technology"></category><category term="Pelican"></category><category term="Server"></category></entry><entry><title>Setting up the Server to host my Pelican Site</title><link href="https://ryancheley.com/2021/07/05/setting-up-the-server-to-host-pelican/" rel="alternate"></link><published>2021-07-05T00:00:00-07:00</published><updated>2021-07-05T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-07-05:/2021/07/05/setting-up-the-server-to-host-pelican/</id><summary type="html">&lt;h1&gt;Creating the user on the server&lt;/h1&gt;
&lt;p&gt;Each site on my server has it's own user. This is a security consideration, more than anything else. For this site, I used the steps from &lt;a href="https://www.ryancheley.com/2021/02/21/automating-the-deployment/"&gt;some of my scripts for setting up a Django site&lt;/a&gt;. In particular, I ran the following code from …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;Creating the user on the server&lt;/h1&gt;
&lt;p&gt;Each site on my server has it's own user. This is a security consideration, more than anything else. For this site, I used the steps from &lt;a href="https://www.ryancheley.com/2021/02/21/automating-the-deployment/"&gt;some of my scripts for setting up a Django site&lt;/a&gt;. In particular, I ran the following code from the shell on the server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;adduser --disabled-password --gecos &amp;quot;&amp;quot; ryancheley

adduser ryancheley www-data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first command above creates the user with no password so that they can't actually log in. It also creates the home directory &lt;code&gt;/home/ryancheley&lt;/code&gt;. This is where the site will be server from.&lt;/p&gt;
&lt;p&gt;The second commands adds the user to the &lt;code&gt;www-data&lt;/code&gt; group. I don't think that's strictly necessary here, but in order to keep this user consistent with the other web site users, I ran it to add it to the group.&lt;/p&gt;
&lt;h1&gt;Creating the nginx config file&lt;/h1&gt;
&lt;p&gt;For the most part I cribbed the &lt;code&gt;nginx&lt;/code&gt; config files from this &lt;a href="https://michael.lustfield.net/nginx/blog-with-pelican-and-nginx"&gt;blog post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There were some changes that were required though. As I indicated in part 1, I had several requirements I was trying to fulfill, most notably not breaking historic links.&lt;/p&gt;
&lt;p&gt;Here is the config file for my UAT site (the only difference between this and the prod site is the server name on line 3):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;server&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;server_name&lt;span class="w"&gt; &lt;/span&gt;uat.ryancheley.com&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;/home/ryancheley/output&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;location&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Serve a .gz version if it exists&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;gzip_static&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;error_page&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/404.html&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;^/index.php/&lt;span class="o"&gt;(&lt;/span&gt;.*&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;permanent&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/favicon.ico&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# This never changes, so don&amp;#39;t let it expire&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;expires&lt;span class="w"&gt; &lt;/span&gt;max&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="w"&gt;    &lt;/span&gt;location&lt;span class="w"&gt; &lt;/span&gt;^~&lt;span class="w"&gt; &lt;/span&gt;/theme&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# This content should very rarely, if ever, change&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;expires&lt;span class="w"&gt; &lt;/span&gt;1y&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;listen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;::&lt;span class="o"&gt;]&lt;/span&gt;:443&lt;span class="w"&gt; &lt;/span&gt;ssl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ipv6only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;listen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssl&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssl_certificate&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/uat.ryancheley.com/fullchain.pem&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssl_certificate_key&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/uat.ryancheley.com/privkey.pem&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;include&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/options-ssl-nginx.conf&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssl_dhparam&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/ssl-dhparams.pem&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;

server&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uat.ryancheley.com&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;301&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;https://&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;



&lt;span class="w"&gt;    &lt;/span&gt;listen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;::&lt;span class="o"&gt;]&lt;/span&gt;:80&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;listen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;server_name&lt;span class="w"&gt; &lt;/span&gt;uat.ryancheley.com&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;


&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most interesting part of the code above is the &lt;code&gt;location&lt;/code&gt; block from lines 6 - 11.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;location&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Serve a .gz version if it exists&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;gzip_static&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;error_page&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/404.html&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;^/index.php/&lt;span class="o"&gt;(&lt;/span&gt;.*&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;permanent&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Custom 404 Page&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;error_page&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/404.html&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This line is what allows me to have a custom &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404"&gt;404&lt;/a&gt; error page. If a page is not found &lt;code&gt;nginx&lt;/code&gt; will serve up the html page &lt;code&gt;404.html&lt;/code&gt; which is generated by a markdown file in my pages directory and looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    Title: Not Found
    Status: hidden
    Save_as: 404.html

    The requested item could not be located.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I got this implementation idea from the &lt;a href="https://docs.getpelican.com/en/4.6.0/tips.html?highlight=404#custom-404-pages"&gt;Pelican docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Rewrite rule for index.php in the URL&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;^/index.php/&lt;span class="o"&gt;(&lt;/span&gt;.*&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;permanent&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The rewrite line fixes the &lt;code&gt;index.php&lt;/code&gt; challenge I mentioned in the &lt;a href="https://www.ryancheley.com/2021/07/02/migrating-to-pelican-from-wordpress/"&gt;previous post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It took me a &lt;em&gt;really&lt;/em&gt; long time to figure this out because the initial config file had a &lt;code&gt;location&lt;/code&gt; block that looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Instead of handling the index, just&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# rewrite / to /index.html&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;^&lt;span class="w"&gt; &lt;/span&gt;/index.html&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I didn't recognize the &lt;code&gt;location = / {&lt;/code&gt; on line 1 as being different than the &lt;code&gt;location&lt;/code&gt; block above starting at line 6. So I added&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;^/index.php/&lt;span class="o"&gt;(&lt;/span&gt;.*&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;permanent&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;to that block and it NEVER worked because it never could.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;=&lt;/code&gt; in the location block indicates a literal exact match, which the regular expression couldn't do because it's trying to be dynamic, but the &lt;code&gt;=&lt;/code&gt; indicates static 🤦🏻‍♂️&lt;/p&gt;
&lt;p&gt;OK, we've got a user, and we've got a configuration file, now all we need is a way to get the files to the server.&lt;/p&gt;
&lt;p&gt;I'll go over that in the next post.&lt;/p&gt;</content><category term="technology"></category><category term="Pelican"></category><category term="Server"></category></entry><entry><title>Migrating to Pelican from Wordpress</title><link href="https://ryancheley.com/2021/07/02/migrating-to-pelican-from-wordpress/" rel="alternate"></link><published>2021-07-02T00:00:00-07:00</published><updated>2021-07-02T00:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-07-02:/2021/07/02/migrating-to-pelican-from-wordpress/</id><summary type="html">&lt;h2&gt;A little back story&lt;/h2&gt;
&lt;p&gt;In October of 2017 I &lt;a href="https://www.ryancheley.com/2017/10/01/migrating-from-square-space-to-word-press/"&gt;wrote about how I migrated from SquareSpace to Wordpress&lt;/a&gt;. After almost 4 years I’ve decided to migrate again, this time to &lt;a href="https://blog.getpelican.com"&gt;Pelican&lt;/a&gt;. I did a bit of work with Pelican during my &lt;a href="https://www.ryancheley.com/2019/08/31/my-first-project-after-completing-the-100-days-of-web-in-python/"&gt;100 Days of Web Code&lt;/a&gt; back in 2019 …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;A little back story&lt;/h2&gt;
&lt;p&gt;In October of 2017 I &lt;a href="https://www.ryancheley.com/2017/10/01/migrating-from-square-space-to-word-press/"&gt;wrote about how I migrated from SquareSpace to Wordpress&lt;/a&gt;. After almost 4 years I’ve decided to migrate again, this time to &lt;a href="https://blog.getpelican.com"&gt;Pelican&lt;/a&gt;. I did a bit of work with Pelican during my &lt;a href="https://www.ryancheley.com/2019/08/31/my-first-project-after-completing-the-100-days-of-web-in-python/"&gt;100 Days of Web Code&lt;/a&gt; back in 2019.&lt;/p&gt;
&lt;p&gt;A good question to ask is, “why migrate to a new platform” The answer, is that while writing my post &lt;a href="https://www.ryancheley.com/2021/06/13/debugging-setting-up-a-django-project/"&gt;Debugging Setting up a Django Project&lt;/a&gt; I had to go back and make a change. It was the first time I’d ever had to use the WordPress Admin to write anything ... and it was awful.&lt;/p&gt;
&lt;p&gt;My writing and posting workflow involves &lt;a href="https://ulysses.app"&gt;Ulysses&lt;/a&gt; where I write everything in MarkDown. Having to use the WYSIWIG interface and the ‘blocks’ in WordPress just broke my brain. That meant what should have been a slight tweak ended up taking me like 45 minutes.&lt;/p&gt;
&lt;p&gt;I decided to give Pelican a shot in a local environment to see how it worked. And it turned out to work very well for my brain and my writing style.&lt;/p&gt;
&lt;h2&gt;Setting it up&lt;/h2&gt;
&lt;p&gt;I set up a local instance of Pelican using the &lt;a href="https://docs.getpelican.com/en/latest/quickstart.html" title="Quick Start"&gt;Quick Start&lt;/a&gt; guide in the docs.&lt;/p&gt;
&lt;p&gt;Pelican has a CLI utility that converts the xml into Markdown files. This allowed me to export my Wordpress blog content to it’s XML output and save it in the Pelican directory I created.&lt;/p&gt;
&lt;p&gt;I then ran the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pelican&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;wordpress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This created about 140 .md files&lt;/p&gt;
&lt;p&gt;Next, I ran a few &lt;code&gt;Pelican&lt;/code&gt; commands to generate the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pelican content
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and then the local web server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pelican --listen
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I reviewed the page and realized there was a bit of clean up that needed to be done. I had categories of Blog posts that only had 1 article, and were really just a different category that needed to be tagged appropriately. So, I made some updates to the categorization and tagging of the posts.&lt;/p&gt;
&lt;p&gt;I also had some broken links I wanted to clean up so I took the opportunity to check the links on all of the pages and make fixes where needed. I used the library &lt;a href="https://pypi.org/project/LinkChecker/"&gt;LinkChecker&lt;/a&gt; which made the process super easy. It is a CLI that generates HTML that you can then review. Pretty neat.&lt;/p&gt;
&lt;h2&gt;Deploying to a test server&lt;/h2&gt;
&lt;p&gt;The first thing to do was to update my DNS for a new subdomain to point to my UAT server. I use Hover and so it was pretty easy to add the new entry.&lt;/p&gt;
&lt;p&gt;I set uat.ryancheley.com to the IP Address 178.128.188.134&lt;/p&gt;
&lt;p&gt;Next, in order to have UAT serve requests for my new site I need to have a configuration file for Nginx. This &lt;a href="https://michael.lustfield.net/nginx/blog-with-pelican-and-nginx"&gt;post&lt;/a&gt; gave me what I needed as a starting point for the config file. Specifically it gave me the location blocks I needed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    location = / {
        # Instead of handling the index, just
        # rewrite / to /index.html
        rewrite ^ /index.html;
    }

    location / {
        # Serve a .gz version if it exists
        gzip_static on;
        # Try to serve the clean url version first
        try_files $uri.htm $uri.html $uri =404;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that in hand I deployed my pelican site to the server&lt;/p&gt;
&lt;p&gt;The first thing I noticed was that the URLs still had &lt;code&gt;index.php&lt;/code&gt; in them. This is a hold over from how my WordPress URL schemes were set up initially that I never got around to fixing but it’s always something that’s bothered me.&lt;/p&gt;
&lt;p&gt;My blog may not be something that is linked to a ton (or at all?), but I didn’t want to break any links if I didn’t have to, so I decided to investigate Nginx rewrite rules.&lt;/p&gt;
&lt;p&gt;I spent a bit of time trying to get my url to from this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;https://www.ryancheley.com/index.php/2017/10/01/migrating-from-square-space-to-word-press/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;https://www.ryancheley.com/migrating-from-square-space-to-word-press/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;using rewrite rules.&lt;/p&gt;
&lt;p&gt;I gave up after several hours of trying different things. This did lead me to some awesome settings for Pelican that would allow me to retain the legacy Wordpress linking structure, so I updated the settings file to include this line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ARTICLE_URL = &amp;#39;index.php/{date:%Y}/{date:%m}/{date:%d}/{slug}/&amp;#39;
ARTICLE_SAVE_AS = &amp;#39;index.php/{date:%Y}/{date:%m}/{date:%d}/{slug}/index.html&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OK. I still have the &lt;code&gt;index.php&lt;/code&gt; issue, but at least my links won’t break.&lt;/p&gt;
&lt;h3&gt;404 Not Found&lt;/h3&gt;
&lt;p&gt;I starting testing the links on the site just kind of clicking here and there and discovered a couple of things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The menu links didn’t always work&lt;/li&gt;
&lt;li&gt;The 404 page wasn’t styled like I wanted it to me styled&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The pelican documentation has an example for creating your own &lt;a href="https://docs.getpelican.com/en/latest/tips.html?highlight=404#custom-404-pages"&gt;404 pages&lt;/a&gt; which also includes what to update the Nginx config file location block.&lt;/p&gt;
&lt;p&gt;And this is what lead me to discover what I had been doing wrong for the rewrites earlier!&lt;/p&gt;
&lt;p&gt;There are two location blocks in the example code I took, but I didn’t see how they were different.&lt;/p&gt;
&lt;p&gt;The first location block is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    location = / {
        # Instead of handling the index, just
        # rewrite / to /index.html
        rewrite ^ /index.html;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Per the Nginx documentation the &lt;code&gt;=&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;If an equal sign is used, this block will be considered a match if the request URI exactly matches the location given.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;BUT since I was trying to use a regular expression, it wasn’t matching exactly and so it wasn’t ‘working’&lt;/p&gt;
&lt;p&gt;The second location block was not an exact match (notice there is no &lt;code&gt;=&lt;/code&gt; in the first line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;location / {
        # Serve a .gz version if it exists
        gzip_static on;
        # Try to serve the clean url version first
        try_files $uri.htm $uri.html $uri =404;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When I added the error page setting for Pelican I also added the URL rewrite rules to remove the &lt;code&gt;index.php&lt;/code&gt; and suddenly my dream of having the redirect rules worked!&lt;/p&gt;
&lt;p&gt;Additionally, I didn’t need the first location block at all. The final location block looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    location / {
        # Serve a .gz version if it exists
        gzip_static on;
        # Try to serve the clean url version first
        # try_files $uri.htm $uri.html $uri =404;
        error_page 404 /404.html;
        rewrite ^/index.php/(.*) /$1  permanent;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I was also able to update my Pelican settings to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ARTICLE_URL = &amp;#39;{date:%Y}/{date:%m}/{date:%d}/{slug}/&amp;#39;
ARTICLE_SAVE_AS = &amp;#39;{date:%Y}/{date:%m}/{date:%d}/{slug}/index.html&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Victory!&lt;/p&gt;
&lt;h2&gt;What I hope to gain from moving&lt;/h2&gt;
&lt;p&gt;In my post outlining the move from SquareSpace to Wordpress I said,&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;As I wrote earlier my main reason for leaving Square Space was the difficulty I had getting content in. So, now that I’m on a WordPress site, what am I hoping to gain from it?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Easier to post my writing&lt;/li&gt;
&lt;li&gt;See Item 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Writing is already really hard for me. I struggle with it and making it difficult to get my stuff out into the world makes it that much harder. My hope is that not only will I write more, but that my writing will get better because I’m writing more.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, what am I hoping to gain from this move:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Just as easy to write my posts&lt;/li&gt;
&lt;li&gt;Easier to edit my posts&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Writing is still hard for me (nearly 4 years later) and while moving to a new shiny tool won’t make the thinking about writing any easier, maybe it will make the process of writing a little more fun and that may lead to more words!&lt;/p&gt;
&lt;h2&gt;Addendum&lt;/h2&gt;
&lt;p&gt;There are already a lot of words here and I have more to say on this. I plan on writing a couple of more posts about the migration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setting up the server to host Pelican&lt;/li&gt;
&lt;li&gt;The writing workflow used&lt;/li&gt;
&lt;/ol&gt;</content><category term="technology"></category><category term="WordPress"></category><category term="Pelican"></category></entry><entry><title>Debugging Setting up a Django Project</title><link href="https://ryancheley.com/2021/06/13/debugging-setting-up-a-django-project/" rel="alternate"></link><published>2021-06-13T11:00:00-07:00</published><updated>2021-06-13T11:00:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-06-13:/2021/06/13/debugging-setting-up-a-django-project/</id><summary type="html">&lt;p&gt;Normally when I start a new Django project I’ll use the PyCharm setup wizard, but recently I wanted to try out VS Code for a Django project and was super stumped when I would get a message like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Normally when I start a new Django project I’ll use the PyCharm setup wizard, but recently I wanted to try out VS Code for a Django project and was super stumped when I would get a message like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;
&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha1&lt;/span&gt;
&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha224&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha224&lt;/span&gt;
&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;
&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha384&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha384&lt;/span&gt;
&lt;span class="nl"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha512&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__func_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__get_builtin_constructor&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;raise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nl"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sha512&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here are the steps I was using to get started&lt;/p&gt;
&lt;p&gt;From a directory I wanted to create the project I would set up my virtual environment&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python3 -m venv venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then activate it&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, I would install Django&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip install django
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, using the &lt;code&gt;startproject&lt;/code&gt; command per the &lt;a href="https://docs.djangoproject.com/en/3.2/ref/django-admin/#startproject" title="Start a new Django Project"&gt;docs&lt;/a&gt; I would&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;django-admin startproject my_great_project .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And get the error message above 🤦🏻‍♂️&lt;/p&gt;
&lt;p&gt;The strangest part about the error message is that it references Python2.7 everywhere … which is odd because I’m in a Python3 virtual environment.&lt;/p&gt;
&lt;p&gt;I did a &lt;code&gt;pip list&lt;/code&gt; and got:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Package    Version
---------- -------
asgiref    3.3.4
Django     3.2.4
pip        21.1.2
pytz       2021.1
setuptools 49.2.1
sqlparse   0.4.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OK … so everything is in my virtual environment. Let’s drop into the REPL and see what’s going on&lt;/p&gt;
&lt;p&gt;&lt;img alt="REPL" class="wp-image-506" src="/images/uploads/2021/06/Screen-Shot-2021-06-13-at-7.52.36-AM.png"&gt;&lt;/p&gt;
&lt;p&gt;Well, that looks to be OK.&lt;/p&gt;
&lt;p&gt;Next, I checked the contents of my directory using &lt;code&gt;tree -L 2&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;├── manage.py
├── my_great_project
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── venv
    ├── bin
    ├── include
    ├── lib
    └── pyvenv.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yep … that looks good too.&lt;/p&gt;
&lt;p&gt;OK, let’s go look at the installed packages for Python 2.7 then. On macOS they’re installed at&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;python2&lt;/span&gt;&lt;span class="m m-Double"&gt;.7&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Looking in there and I see that Django is installed.&lt;/p&gt;
&lt;p&gt;OK, let’s use pip to uninstall Django from Python2.7, except that &lt;code&gt;pip&lt;/code&gt; gives essentially the same result as running the &lt;code&gt;django-admin&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;OK, let’s just remove it manually. After a bit of googling I found this &lt;a href="https://stackoverflow.com/a/8146552"&gt;Stackoverflow&lt;/a&gt; answer on how to remove the offending package (which is what I assumed would be the answer, but better to check, right?)&lt;/p&gt;
&lt;p&gt;After removing the &lt;code&gt;Django&lt;/code&gt; install from Python 2.7 and running &lt;code&gt;django-admin --version&lt;/code&gt; I get&lt;/p&gt;
&lt;p&gt;&lt;img alt="Django-admin --version" class="wp-image-507" src="/images/uploads/2021/06/Screen-Shot-2021-06-13-at-8.05.55-AM.png"&gt;&lt;/p&gt;
&lt;p&gt;So I googled that error message and found another answers on &lt;a href="https://stackoverflow.com/a/10756446"&gt;Stackoverflow&lt;/a&gt; which lead me to look at the &lt;code&gt;manage.py&lt;/code&gt; file. When I &lt;code&gt;cat&lt;/code&gt; the file I get:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# manage.py&lt;/span&gt;

&lt;span class="c1"&gt;#!/usr/bin/env python&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That first line SHOULD be finding the Python executable in my virtual environment, but it’s not.&lt;/p&gt;
&lt;p&gt;Next I googled the error message &lt;code&gt;django-admin code for hash sha384 was not found&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Which lead to this &lt;a href="https://stackoverflow.com/a/60575879"&gt;Stackoverflow&lt;/a&gt; answer. I checked to see if Python2 was installed with brew using&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;brew leaves | grep python
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which returned &lt;code&gt;python@2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Based on the answer above, the solution was to uninstall the Python2 that was installed by &lt;code&gt;brew&lt;/code&gt;. Now, although &lt;a href="https://www.python.org/doc/sunset-python-2/"&gt;Python2 has retired&lt;/a&gt;, I was leery of uninstalling it on my system without first verifying that I could remove the brew version without impacting the system version which is needed by macOS.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;brew info python@2&lt;/code&gt; I determined where &lt;code&gt;brew&lt;/code&gt; installed Python2 and compared it to where Python2 is installed by macOS and they are indeed different&lt;/p&gt;
&lt;p&gt;Output of &lt;code&gt;brew info python@2&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Cellar&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="mi"&gt;@2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;2.7.15&lt;/span&gt;&lt;span class="n"&gt;_1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;515&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;122.4&lt;/span&gt;&lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Built&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2018-08&lt;/span&gt;&lt;span class="mo"&gt;-05&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output of &lt;code&gt;which python&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr/bin/python&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;OK, now we can remove the version of Python2 installed by &lt;code&gt;brew&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;brew&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uninstall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="mi"&gt;@2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now with all of that cleaned up, lets try again. From a clean project directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate
pip install django
django-admin --version
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last command returned&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nl"&gt;zsh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="mi"&gt;@2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python2&lt;/span&gt;&lt;span class="mf"&gt;.7&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;such&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;
&lt;span class="mf"&gt;3.2.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OK, I can get the version number and it mostly works, but can I create a new project?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;django-admin startproject my_great_project .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which returns&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nl"&gt;zsh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="mi"&gt;@2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python2&lt;/span&gt;&lt;span class="mf"&gt;.7&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;such&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;BUT, the project was installed&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;├── db.sqlite3
├── manage.py
├── my_great_project
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── venv
    ├── bin
    ├── include
    ├── lib
    └── pyvenv.cfg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And I was able to run it&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Django Debug Homepage" class="wp-image-508" src="/images/uploads/2021/06/Screen-Shot-2021-06-13-at-9.01.19-AM.png"&gt;&lt;/p&gt;
&lt;p&gt;Success! I’ve still got that last bug to deal with, but that’s a story for a different day!&lt;/p&gt;
&lt;h2&gt;Short Note&lt;/h2&gt;
&lt;p&gt;My initial fix, and my initial draft for this article, was to use the old adage, turn it off and turn it back on. In this case, the implementation would be the &lt;code&gt;deactivate&lt;/code&gt; and then re &lt;code&gt;activate&lt;/code&gt; the virtual environment and that’s what I’d been doing.&lt;/p&gt;
&lt;p&gt;As I was writing up this article I was hugely influenced by the work of &lt;a href="https://twitter.com/b0rk"&gt;Julie Evans&lt;/a&gt; and kept asking, “but why?”. She’s been writing a lot of awesome, amazing things, and has several &lt;a href="https://wizardzines.com"&gt;zines for purchase&lt;/a&gt; that I would highly recommend.&lt;/p&gt;
&lt;p&gt;She’s also generated a few &lt;a href="https://jvns.ca/blog/2021/04/16/notes-on-debugging-puzzles/"&gt;debugging ‘games’&lt;/a&gt; that are a lot of fun.&lt;/p&gt;
&lt;p&gt;Anyway, thanks Julie for pushing me to figure out the why for this issue.&lt;/p&gt;
&lt;h2&gt;Post Script&lt;/h2&gt;
&lt;p&gt;I figured out the error message above and figured, well, I might as well update the post! I thought it had to do with &lt;code&gt;zsh&lt;/code&gt;, but no, it was just more of the same.&lt;/p&gt;
&lt;p&gt;The issue was that Django had been installed in the base Python2 (which I knew). All I had to do was to uninstall it with pip.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip uninstall django
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The trick was that pip wasn't working out for me ... it was generating errors. So I had to run the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python -m pip uninstall django
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I had to run this AFTER I put the Django folder back into &lt;code&gt;/usr/local/lib/python2.7/site-packages&lt;/code&gt; (if you'll recall from above, I removed it from the folder)&lt;/p&gt;
&lt;p&gt;After that clean up was done, everything worked out as expected! I just had to keep digging!&lt;/p&gt;</content><category term="technology"></category><category term="Debugging"></category><category term="macOS"></category><category term="python"></category></entry><entry><title>My First Python Package</title><link href="https://ryancheley.com/2021/06/06/my-first-python-package/" rel="alternate"></link><published>2021-06-06T18:11:00-07:00</published><updated>2021-06-06T18:11:00-07:00</updated><author><name>ryan</name></author><id>tag:ryancheley.com,2021-06-06:/2021/06/06/my-first-python-package/</id><summary type="html">&lt;p&gt;A few months ago I was inspired by &lt;a href="https://simonwillison.net" title="Simon, creator of Datasette"&gt;Simon Willison&lt;/a&gt; and his project &lt;a href="https://datasette.io" title="Datasette - An awesome tool for data exploration and publishing"&gt;Datasette&lt;/a&gt; and it’s related ecosystem to write a Python Package for it.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://toggl.com" title="Toggl - a time tracking tool"&gt;toggl&lt;/a&gt; to track my time at work and I thought this would be a great opportunity use that data with &lt;a href="https://datasette.io" title="Datasette - An awesome tool for data exploration and publishing"&gt;Datasette&lt;/a&gt; and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A few months ago I was inspired by &lt;a href="https://simonwillison.net" title="Simon, creator of Datasette"&gt;Simon Willison&lt;/a&gt; and his project &lt;a href="https://datasette.io" title="Datasette - An awesome tool for data exploration and publishing"&gt;Datasette&lt;/a&gt; and it’s related ecosystem to write a Python Package for it.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://toggl.com" title="Toggl - a time tracking tool"&gt;toggl&lt;/a&gt; to track my time at work and I thought this would be a great opportunity use that data with &lt;a href="https://datasette.io" title="Datasette - An awesome tool for data exploration and publishing"&gt;Datasette&lt;/a&gt; and see if I couldn’t answer some interesting questions, or at the very least, do some neat data discovery.&lt;/p&gt;
&lt;p&gt;The purpose of this package is to:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a SQLite database containing data from your &lt;a href="https://toggl.com" title="Toggl - a time tracking tool"&gt;toggl&lt;/a&gt; account&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I followed the &lt;a href="https://packaging.python.org/tutorials/packaging-projects/" title="How do I add a package to PyPi?"&gt;tutorial for committing a package to PyPi&lt;/a&gt; and did the first few pushes manually. Then, using a GitHub action from one of Simon’s &lt;a href="https://datasette.io" title="Datasette - An awesome tool for data exploration and publishing"&gt;Datasette&lt;/a&gt; projects, I was able to automate it when I make a release on GitHub!&lt;/p&gt;
&lt;p&gt;Since the initial commit on March 7 (my birthday BTW) I’ve had 10 releases, with the most recent one coming yesterday which removed an issue with one of the tables reporting back an API key which, if published on the internet could be a bad thing ... so hooray for security enhancements!&lt;/p&gt;
&lt;p&gt;Anyway, it was a fun project, and got me more interested in authoring Python packages. I’m hoping to do a few more related to &lt;a href="https://datasette.io"&gt;Datasette&lt;/a&gt; (although I’m not sure what to write honestly!).&lt;/p&gt;
&lt;p&gt;Be sure to check out the package on &lt;a href="https://pypi.org/project/toggl-to-sqlite/" title="toggl-to-SQLite"&gt;PyPi.org&lt;/a&gt; and the source code on &lt;a href="https://github.com/ryancheley/toggl-to-sqlite/" title="GitHub repo of toggl-to-sqlite"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content><category term="technology"></category><category term="datasette"></category><category term="Python"></category><category term="python package"></category></entry></feed>