<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jon Allured</title>
  <id>https://www.jonallured.com/posts</id>
  <link href="https://www.jonallured.com/posts"/>
  <link href="https://www.jonallured.com/atom.xml" rel="self"/>
  <updated>2026-03-03T09:04:00-06:00</updated>
  <author>
    <name>Jon Allured</name>
  </author>
  
  <entry>
    <title>Verify Recurring Job Schedules With a Spec</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/03/03/verify-recurring-job-schedules-with-a-spec.html"/>
    <id>https://www.jonallured.com/posts/2026/03/03/verify-recurring-job-schedules-with-a-spec.html</id>
    <published>2026-03-03T09:04:00-06:00</published>
    <updated>2026-03-03T09:04:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;My Ruby on Rails app &lt;a href=&quot;https://github.com/jonallured/monolithium&quot;&gt;Monolithium&lt;/a&gt; uses &lt;a href=&quot;https://github.com/rails/solid_queue&quot;&gt;Solid Queue&lt;/a&gt; for background jobs
but also has some recurring jobs. When I &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/258&quot;&gt;migrated to Solid
Queue&lt;/a&gt; I typoed the schedule strings for these recurring jobs
and then had to &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/259&quot;&gt;make a PR to fix them&lt;/a&gt;. This felt like a silly
mistake and seemed like something that could have easily been prevented. I made
a mental note but moved on with my life.&lt;/p&gt;

&lt;p&gt;Then this verification topic came up at work this week. I addressed it there and
then took the opportunity to apply it to my Rails app as well.&lt;/p&gt;

&lt;h2&gt;Solid Queue Recurring Job Schedules&lt;/h2&gt;

&lt;p&gt;The recurring jobs that are managed by Solid Queue are configured with a YAML
file that looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# config/recurring.yml

production:
  name_of_job:
    class: MyBestJob
    schedule: every 10 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The top-most key is the environment to run so here I&apos;m using &lt;code&gt;production&lt;/code&gt; but
you could also specify jobs just for &lt;code&gt;development&lt;/code&gt; but I never have. From there
the next level of key is the name of the recurring job or task that you want to
define. Then inside that key you specify the &lt;code&gt;class&lt;/code&gt; and &lt;code&gt;schedule&lt;/code&gt; and any
additional configuration. What I&apos;ve specified here is that every 10 minutes the
&lt;code&gt;MyBestJob&lt;/code&gt; class should be enqueued and worked.&lt;/p&gt;

&lt;p&gt;For that &lt;code&gt;schedule&lt;/code&gt; string here&apos;s what the Solid Queue docs have to say:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Each task needs to have also a schedule, which is parsed using Fugit, so it
accepts anything that Fugit accepts as a cron.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ah ha so now we know how these schedule strings are used - they are passed off
to &lt;a href=&quot;https://github.com/floraison/fugit&quot;&gt;Fugit&lt;/a&gt; and whatever that gem accepts is allowed here.&lt;/p&gt;

&lt;h2&gt;Fugit Parsing&lt;/h2&gt;

&lt;p&gt;The Fugit gem can parse a lot of different types of inputs. It can take
something high level like &quot;every 10 minutes&quot; but then it can also take low level
cron strings like &quot;5 4 * * sun&quot; which I just randomly generated from
&lt;a href=&quot;https://crontab.guru&quot;&gt;crontab.guru&lt;/a&gt;. That range of inputs then is inherited by Solid Queue. Neat!&lt;/p&gt;

&lt;h2&gt;Writing a Spec to Verify Schedule Strings&lt;/h2&gt;

&lt;p&gt;With all this in mind the idea for this spec is to read in the config file, grab
the schedule strings, and then parse them with Fugit. I learned from the Fugit
docs that I could use &lt;code&gt;Fugit.parse&lt;/code&gt; for this and it will return nil if the input
string cannot be parsed.&lt;/p&gt;

&lt;p&gt;I made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/302&quot;&gt;Verify recurring job schedules with a spec&lt;/a&gt; and here&apos;s
what I ended up with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;describe &quot;recurring schedules&quot; do
  it &quot;only has valid schedule strings&quot; do
    config_path = Rails.root.join(&quot;config/recurring.yml&quot;)
    recurring_config = ActiveSupport::ConfigurationFile.parse(config_path)
    tasks = recurring_config.values.map(&amp;amp;:values).flatten
    schedules = tasks.map { |task| task[&quot;schedule&quot;] }
    schedules.each do |schedule|
      expect(Fugit.parse(schedule)).to_not eq nil
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What this spec does is use the &lt;code&gt;ActiveSupport::ConfigurationFile&lt;/code&gt; helper to grab
the contents of the recurring jobs config file. That will already have converted
it into a hash so then I deconstruct that hash and pluck out the values of the
&lt;code&gt;schedule&lt;/code&gt; keys and make an array out of that. Then I loop over that array and
send each string to the &lt;code&gt;Fugit.parse&lt;/code&gt; method with an assertion that the result
is not nil. If the config file is valid then the test passes. If I edit the file
and use a schedule that is invalid then the test fails. Confidence restored!&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;This is exactly what I wanted - a safety net that will ensure I do not repeat
the mistake of using an incorrect schedule string. I&apos;m not fully satisfied
though. There are two things bothering me: the lack of help when the test fails
and that Solid Queue should be helping me here.&lt;/p&gt;

&lt;p&gt;For that first one what I noticed is that if I intentionally make the test fail
then the error message is all about how something is nil. What I think a better
test would do is give me which schedule broke. I bet I could write something
like a custom matcher for this but who has the time??&lt;/p&gt;

&lt;p&gt;Then the other thing is that honestly why doesn&apos;t Solid Queue give me something
to work with here? If I had the idea for this spec then I&apos;m sure others have as
well. In my imagination there are all these teams using Solid Queue and writing
the exact same type of test - what a waste! Many Rails related gems have a test
helper or test mode or something like that. I guess if I really cared about this
what I would do is open up an issue or PR on that project but who has the time??&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Week in Review: Week 9, 2026</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/03/01/week-in-review-week-9-2026.html"/>
    <id>https://www.jonallured.com/posts/2026/03/01/week-in-review-week-9-2026.html</id>
    <published>2026-03-01T15:52:00-06:00</published>
    <updated>2026-03-01T15:52:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;I ended up taking Monday off this week because I hurt my back and needed to work
on making it feel better. What I would like to tell you is that I hurt it
because I was doing something awesome. Sky-diving or whatever. Nope. I hurt it
because I slept funny. That&apos;s how old I am.&lt;/p&gt;

&lt;h2&gt;Highlights&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;worked 32:00, 8:00 PTO&lt;/li&gt;
  &lt;li&gt;published &lt;a href=&quot;https://www.jonallured.com/posts/2026/02/25/show-timestamps-in-local-zone.html&quot;&gt;Show Timestamps in Local Zone&lt;/a&gt; on here&lt;/li&gt;
  &lt;li&gt;finished &lt;a href=&quot;https://www.terrypratchettbooks.com/books/the-colour-of-magic/&quot;&gt;The Color of Magic&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;finished &lt;a href=&quot;https://www.davidsedarisbooks.com/titles/david-sedaris/dress-your-family-in-corduroy-and-denim/9780316143462/&quot;&gt;Dress Your Family in Corduroy and Denim&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;created &lt;a href=&quot;https://github.com/search?order=asc&amp;amp;q=author%3A%40me+created%3A2026-02-22..2026-02-28+type%3Apr&amp;amp;sort=created&amp;amp;type=issues&quot;&gt;14 Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wow can you believe I finished 2 books in a week? Ok I actually did not. I
listened to The Color of Magic on Audible and that Sedaris book was short so I
flew through it. Both were great!&lt;/p&gt;

&lt;p&gt;Oh and The Color of Magic is the first book in a series called Discworld. I
kinda regretted listening to it because I think I missed some of the jokes. The
next one I read will be in print.&lt;/p&gt;

&lt;h2&gt;Pic of the Week&lt;/h2&gt;

&lt;p&gt;Jack made this:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-98/snowman-full.jpg&quot;&gt;
    &lt;img alt=&quot;Snowman&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-98/snowman-full.jpg&quot; srcset=&quot;/images/post-98/snowman-900.jpg 900w, /images/post-98/snowman-1200.jpg 1200w, /images/post-98/snowman-1800.jpg 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Please note that this snowman has a mohawk.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Good luck falling asleep tonight and if you do then expect this monster will
haunt your dreams. I mean nightmares.&lt;/p&gt;

&lt;h2&gt;Next Week&lt;/h2&gt;

&lt;p&gt;This week coming up is a short one for me because I&apos;m taking a few days off work
to do a short trip to AZ. My Grandma is turning 90 and a bunch of us are going
to celebrate with her. I cannot wait to get out of the cold for a few days.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Show Timestamps in Local Zone</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/02/25/show-timestamps-in-local-zone.html"/>
    <id>https://www.jonallured.com/posts/2026/02/25/show-timestamps-in-local-zone.html</id>
    <published>2026-02-25T16:44:00-06:00</published>
    <updated>2026-02-25T16:44:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;My Ruby on Rails application &lt;a href=&quot;https://github.com/jonallured/monolithium/&quot;&gt;Monolithium&lt;/a&gt; stores times in UTC and thus far
I&apos;ve been content to show them without considering timezone. As an example, the
&lt;a href=&quot;https://app.jonallured.com/boops&quot;&gt;Boops Page&lt;/a&gt; will show the &lt;code&gt;created_at&lt;/code&gt; timestamp but it&apos;s just in UTC.
This annoyed me so I decided to look into what it would take to show them in a
user&apos;s local timezone.&lt;/p&gt;

&lt;h2&gt;How Should I Do This?&lt;/h2&gt;

&lt;p&gt;The first questions I hit were around where I would do the conversion from UTC
to another timezone and which other timezone should I use. Should I do the
conversion client side or server side?  What if I just used my own timezone
since this is largely a personal app? Oh maybe I should only do this when I&apos;m
signed in? Could I use a cookie to set the timezone and pass that into the
server side code?&lt;/p&gt;

&lt;h2&gt;There&apos;s a Gem For This&lt;/h2&gt;

&lt;p&gt;While pondering these questions I did some searching and found that there was
already a gem for this! I found &lt;a href=&quot;https://github.com/basecamp/local_time&quot;&gt;local_time&lt;/a&gt; from &lt;a href=&quot;https://basecamp.com&quot;&gt;Basecamp&lt;/a&gt; which seemed
close enough to official that I should just use it. Plus it answered many of
these questions.&lt;/p&gt;

&lt;p&gt;How it works is that it provides view helpers that write your timestamps into
&lt;code&gt;time&lt;/code&gt; elements with data attributes. Then it also includes some Javascript that
runs client side and will process these &lt;code&gt;time&lt;/code&gt; elements with logic that converts
the timestamp into the user&apos;s local timezone. Neat!&lt;/p&gt;

&lt;p&gt;I decided to try it out and made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/296&quot;&gt;Localize times with local time
gem&lt;/a&gt; which demonstrates how to use it for my case. It wasn&apos;t
hard - I did end up making my own helper method that wraps the helper method
that the gem provides:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;def in_tz(time)
  local_time(time, Time::DATE_FORMATS[:default])
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I did this because I always wanted to use that format and it made the view code
much more simple:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;%p= in_tz something.created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Refused Bequest Like Crazy&lt;/h2&gt;

&lt;p&gt;At this point I noticed that the diff included a Javascript file in the vendor
folder and that got me thinking about what this gem was actually doing. I
started poking around and reading more of the README. The features of this gem
that I was not using started piling up:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;helpers for both times and dates&lt;/li&gt;
  &lt;li&gt;adding a &lt;code&gt;title&lt;/code&gt; attribute to the &lt;code&gt;time&lt;/code&gt; element&lt;/li&gt;
  &lt;li&gt;support for arbitrary formats&lt;/li&gt;
  &lt;li&gt;integration with i18n&lt;/li&gt;
  &lt;li&gt;timezone edge cases&lt;/li&gt;
  &lt;li&gt;helpers for relative time that are updated every minute&lt;/li&gt;
  &lt;li&gt;support for 24-hour vs 12-hour times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yeah it wasn&apos;t long before it dawned on me that I was barely using any of the
functionality that this gem provided. &lt;a href=&quot;https://refactoring.guru/smells/refused-bequest&quot;&gt;Refused Bequest&lt;/a&gt; like crazy. Isn&apos;t
there a better way?&lt;/p&gt;

&lt;h2&gt;Making My Own Solution&lt;/h2&gt;

&lt;p&gt;I checked out a new branch and reversed the install of the gem. I left the
changes to the view files and set my sights on what it would take to arrive at
the same outcome but with much less code.&lt;/p&gt;

&lt;p&gt;Here&apos;s what I had in mind:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;update the helper method to create a &lt;code&gt;time&lt;/code&gt; tag kinda like the gem does&lt;/li&gt;
  &lt;li&gt;add a data attribute to that tag that tracks if it has been converted yet&lt;/li&gt;
  &lt;li&gt;add a Javascript event listener for when the page is loaded&lt;/li&gt;
  &lt;li&gt;create a function to find all the &lt;code&gt;time&lt;/code&gt; tags and convert them&lt;/li&gt;
  &lt;li&gt;update the data attribute once done converting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having the code of the gem to refer to made it actually pretty easy. I did this
and made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/297&quot;&gt;Localize times with intz&lt;/a&gt; which is a much more simple
and smaller change! By cutting out most of the features of the gem and finding
the smallest bit of functionality that I actually needed for my use case I
arrived at something I can understand and easily maintain.&lt;/p&gt;

&lt;p&gt;It isn&apos;t tested though - don&apos;t tell anyone!&lt;/p&gt;

&lt;h2&gt;Formatting Times in Javascript&lt;/h2&gt;

&lt;p&gt;Back to the code for a second - the function that converts the &lt;code&gt;time&lt;/code&gt; elements
is pretty small and I think easy to understand:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const intz = () =&amp;gt; {
  const timeElements = document.querySelectorAll(&quot;time[data-intz=&apos;false&apos;]&quot;)

  for (const timeElement of timeElements) {
    const datetime = timeElement.getAttribute(&quot;datetime&quot;)
    const localTime = new Date(Date.parse(datetime))
    const localText = formatTime(localTime)
    timeElement.innerText = localText
    timeElement.setAttribute(&quot;data-intz&quot;, &quot;true&quot;)
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Find all the &lt;code&gt;time&lt;/code&gt; elements and loop through them. For each element pull out
the timestamp in UTC, convert it to a local time that is formatted nicely,
update the text of the element, and then update the data attribute. Pass that
function to the event handler for &lt;code&gt;turbo:load&lt;/code&gt; and boom, timestamps get
converted!&lt;/p&gt;

&lt;p&gt;Note that I originally had been using &lt;code&gt;DOMContentLoaded&lt;/code&gt; but then noticed Turbo
was getting in the way and made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/298&quot;&gt;Use Turbo event to localize
times&lt;/a&gt; to switch the event.&lt;/p&gt;

&lt;p&gt;What ended up being the worst part of this was that &lt;code&gt;formatTime&lt;/code&gt; function. Did
you know that formatting a &lt;code&gt;Date&lt;/code&gt; object in Javascript sucks? It does if you
don&apos;t want to use a library! There is nothing like &lt;code&gt;strftime&lt;/code&gt; and so I had to
build something myself. Pain points included:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;month is zero-indexed so you have to add 1&lt;/li&gt;
  &lt;li&gt;you get the month with a function called &lt;code&gt;getMonth&lt;/code&gt; but you get day with
&lt;code&gt;getDate&lt;/code&gt; - the &lt;code&gt;getDay&lt;/code&gt; function returns the number of the day of the week??&lt;/li&gt;
  &lt;li&gt;hour is 24-hour time so you have to convert it to 12-hour time yourself&lt;/li&gt;
  &lt;li&gt;there is nothing to get the am/pm out&lt;/li&gt;
  &lt;li&gt;you have to pad month, day, hour, minute, and second yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As someone that avoids Javascript as much as possible it was life-affirming to
see how stupid this all was. Feel free to have a look at my &lt;a href=&quot;https://github.com/jonallured/monolithium/blob/c893c526a8004385f8cf60d0ca8f0deaa89aa633/app/javascript/intz.js#L3&quot;&gt;formatTime&lt;/a&gt;
function if you&apos;d like to see how I addressed this problem. I&apos;m pretty sure it
works just fine.&lt;/p&gt;

&lt;h2&gt;Assessing Trade-offs&lt;/h2&gt;

&lt;p&gt;A fair question might be &quot;why build this yourself - why not just use the gem?&quot;
and ultimately this is about trade-offs. In my mind I would rather build this
myself because I&apos;m using so little of what the gem does and the code that I
added is not complex enough that I foresee problems maintaining it. That&apos;s the
calculation I&apos;m making. I don&apos;t know, maybe you would make a different one!&lt;/p&gt;

&lt;p&gt;I do wish I had a way to unit test those functions but oh well. I don&apos;t relish
the idea of spinning up an entire Javascript test suite just for one file.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Week in Review: Week 8, 2026</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/02/22/week-in-review-week-8-2026.html"/>
    <id>https://www.jonallured.com/posts/2026/02/22/week-in-review-week-8-2026.html</id>
    <published>2026-02-22T09:34:00-06:00</published>
    <updated>2026-02-22T09:34:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;This was a short week for me with Monday being a holiday. Not much to report
other than I did my best swim this week - 850 meters!&lt;/p&gt;

&lt;p&gt;Maybe worth mentioning is that I have continued to volunteer to purchase some
groceries for families that are too afraid to do it themselves. I get a list and
run over to the store to grab what they need. The groceries come home and I get
them ready for the next day when I drive them over to the collection point. Then
other volunteers deliver them. This is filling my cup and giving me some hope
that the world still contains good people.&lt;/p&gt;

&lt;h2&gt;Highlights&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;worked 32:00, 8:00 PTO&lt;/li&gt;
  &lt;li&gt;finished &lt;a href=&quot;https://en.wikipedia.org/wiki/Mistborn:_The_Hero_of_Ages&quot;&gt;Hero of Ages&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;created &lt;a href=&quot;https://github.com/search?order=asc&amp;amp;q=author%3A%40me+created%3A2026-02-15..2026-02-21+type%3Apr&amp;amp;sort=created&amp;amp;type=issues&quot;&gt;8 Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I continue to work on automating the process of announcing new blog posts on my
socials. This week I landed the modeling in order to keep track of blog posts
and then updated my Bluesky client gem to support these types of announcements.
I learned a bunch about richtext and embeds and how the Bluesky API deals with
them. Next up is Mastodon which looks to be a bit more simple.&lt;/p&gt;

&lt;h2&gt;Some Best Things&lt;/h2&gt;

&lt;p&gt;I finished the Mistborn series by Brandon Sanderson which I really liked! It&apos;s a
fun trilogy that does not take itself too seriously and had enough twists and
turns to keep me wanting to know what happens next. The magic system is unlike
anything else I&apos;ve encountered.&lt;/p&gt;

&lt;p&gt;The other thing I&apos;ll mention here is the &lt;a href=&quot;https://en.wikipedia.org/wiki/Wonder_Man_(miniseries)&quot;&gt;Wonder Man&lt;/a&gt; show which Jess and I
finished. I had been hearing some buzz about it so convinced her to give it a
shot. She mostly humored me and didn&apos;t even stick it out to the final episode
but I thought it was cool. I especially liked the Doorman episode - ding dong!&lt;/p&gt;

&lt;h2&gt;Next Week&lt;/h2&gt;

&lt;p&gt;Should be a normal, quiet week. No more volunteering while they assess what the
community needs going forward. I did upgrade to Ruby 4 on all my projects so
maybe this week I&apos;ll do a Rails upgrade that I&apos;ve been putting off.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Upgrading to Ruby 4.0</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/02/14/upgrading-to-ruby-four-oh.html"/>
    <id>https://www.jonallured.com/posts/2026/02/14/upgrading-to-ruby-four-oh.html</id>
    <published>2026-02-14T11:28:00-06:00</published>
    <updated>2026-02-14T11:28:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;Ruby 4.0 was released a couple months ago on Christmas and I had been meaning to
upgrade for a while. I noticed that version 4.0.1 came out too so that&apos;s always
a good sign that things are stable and ready for prime time.&lt;/p&gt;

&lt;h2&gt;Reviewing Release Notes&lt;/h2&gt;

&lt;p&gt;I started by reviewing the &lt;a href=&quot;https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/&quot;&gt;release post&lt;/a&gt; on the official Ruby
blog. Nothing really stood out as all that controversial and so I figured it
would be a pretty straightforward upgrade. It was.&lt;/p&gt;

&lt;p&gt;But that isn&apos;t to say that as a fan of the Ruby language I&apos;m not excited by what
was in those release notes - there are some cool things in Ruby 4! You should
search that page for &quot;ErrorHighlight&quot; and see what they did to improve the error
messages for &lt;code&gt;ArgumentError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Installing&lt;/h2&gt;

&lt;p&gt;I still use &lt;a href=&quot;https://asdf-vm.com&quot;&gt;asdf&lt;/a&gt; as my Ruby version manager so next was installing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ asdf plugin update --all
$ asdf install ruby 4.0.1
$ asdf list ruby
  3.3.10
 *3.4.8
  4.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So the goal is to upgrade all my projects to Ruby 4.0.1 and then uninstall those
other versions.&lt;/p&gt;

&lt;h2&gt;Setting Default Ruby&lt;/h2&gt;

&lt;p&gt;My dotfiles repo is where I set the default Ruby for my system so I made
&lt;a href=&quot;https://github.com/jonallured/dotfiles/pull/246&quot;&gt;Upgrade default Ruby&lt;/a&gt; to set this to &lt;code&gt;4.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Upgrade All the Projects&lt;/h2&gt;

&lt;p&gt;Now the toil - go to each project and do a PR that upgrades it to Ruby 4.0.1:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/jay/pull/11&quot;&gt;jay&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/jonallured.com/pull/197&quot;&gt;jonallured.com&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/mli/pull/18&quot;&gt;mli&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/290&quot;&gt;monolithium&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/shrt/pull/28&quot;&gt;shrt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mostly the process was:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;edit the &lt;code&gt;.tool-versions&lt;/code&gt; file to switch to 4.0.1&lt;/li&gt;
  &lt;li&gt;run &lt;code&gt;bundle install&lt;/code&gt; to install the project&apos;s gems&lt;/li&gt;
  &lt;li&gt;run &lt;code&gt;bundle update --bundler&lt;/code&gt; to get the newest bundler version&lt;/li&gt;
  &lt;li&gt;run &lt;code&gt;bundle update --all&lt;/code&gt; to upgrade the project&apos;s gems&lt;/li&gt;
  &lt;li&gt;run &lt;code&gt;bundle exec rake&lt;/code&gt; to see if anything broke&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oh and then if a project has a Ruby version matrix on CI then I&apos;d also drop the
3.2 and add 4.0. I actually forgot this on jay so I opened &lt;a href=&quot;https://github.com/jonallured/jay/pull/12&quot;&gt;Update CI
matrix&lt;/a&gt; as a follow up.&lt;/p&gt;

&lt;p&gt;Monolithium was the project that I assumed would be the most challenging to
upgrade but it Just Worked. I did notice that it needs a Rails upgrade and I
have been ignoring the Tailwind upgrade as well. Those are problems for Future
Jon!&lt;/p&gt;

&lt;h2&gt;Cleaning Up&lt;/h2&gt;

&lt;p&gt;This was an easy upgrade and the final step was to clean up my older Ruby
installs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ asdf uninstall ruby 3.3.10
$ asdf uninstall ruby 3.4.8
$ asdf list ruby
 *4.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I love boring software!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Making Time for Boops</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/02/13/making-time-for-boops.html"/>
    <id>https://www.jonallured.com/posts/2026/02/13/making-time-for-boops.html</id>
    <published>2026-02-13T14:44:00-06:00</published>
    <updated>2026-02-13T14:44:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;A couple months back, just before the Christmas holiday break, I received a
&lt;a href=&quot;https://lametric.com/en-US/time/overview&quot;&gt;TIME smart clock&lt;/a&gt; as a gift from Shopify. They gave everybody the choice
between a few different items from merchants on the platform. I thought it was
both a fun way to recognize employee effort and a cool way to dog food Shopify&apos;s
software. Plus merchants earned a little money off the project. Win-win-win.&lt;/p&gt;

&lt;p&gt;Setting it up was easy and fun and I instantly fell in love with the retro
look of the display. Those pixels are so chunky!&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-94/time-on-desk-full.jpg&quot;&gt;
    &lt;img alt=&quot;TIME clock on desk&quot; height=&quot;570&quot; loading=&quot;eager&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-94/time-on-desk-full.jpg&quot; srcset=&quot;/images/post-94/time-on-desk-900.jpg 900w, /images/post-94/time-on-desk-1200.jpg 1200w, /images/post-94/time-on-desk-1800.jpg 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;The default face is the calendar app and it is fine.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Before I headed out for the holidays I messed with the device a little. I
learned enough to kinda understand what was possible. Ideas bloomed but I didn&apos;t
get very far.&lt;/p&gt;

&lt;p&gt;Then this was Hackathon week at Shopify and I decided to take one of those ideas
and run with it.&lt;/p&gt;

&lt;h2&gt;The Idea&lt;/h2&gt;

&lt;p&gt;What I had in mind was something where I&apos;d make a public page on my Rails app
where people could click a button and send a message to appear on the device. As
I pondered the idea over the couple months, a name for the idea came to me:
Boops. Once you have a good name how can you not build it right??&lt;/p&gt;

&lt;p&gt;So yeah, people would create a Boop and send it to me. Fun!&lt;/p&gt;

&lt;h2&gt;LaMetric Developer Account&lt;/h2&gt;

&lt;p&gt;As I sat down to start really digging into this concept and get something built
I first had to create a developer account with LaMetric, the company that
manufactures the TIME smart clock. It was just a free sign-up and I was in, no
big deal. But the screen you land on when creating your account presents you
with 3 choices of apps to create:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Indicator App&lt;/li&gt;
  &lt;li&gt;Button App&lt;/li&gt;
  &lt;li&gt;Notification App&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I had no clue! Luckily there was a link to the &lt;a href=&quot;https://lametric-documentation.readthedocs.io/en/latest/index.html&quot;&gt;LaMetric Developer
Documentation&lt;/a&gt; and there I spent quite a bit of time reading and tinkering
with things to figure out what I wanted to do.&lt;/p&gt;

&lt;h2&gt;Frames and Clicks&lt;/h2&gt;

&lt;p&gt;Ultimately I decided to build an Indicator App. What I needed to do was create 2
API endpoints - one for frames (what to display on the device) and one for
clicks (what to do when the button on top is pressed). The device sends GET
requests to these endpoints and will error unless it gets a 200 back. This is
what I sketched out:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/v1/time_clock/frames
GET /api/v1/time_clock/clicks
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;My Rails app Monolithium already had good patterns for adding API endpoints so
making &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/280&quot;&gt;Add endpoints for time clock&lt;/a&gt; was not hard. At this
point the frames endpoint just returned a static default frame and the click
endpoint did nothing. But it was enough to complete the form in the developer
portal, get verified, publish the Indicator App, and install it on my device.&lt;/p&gt;

&lt;p&gt;I tested it and it worked - I tailed my Heroku logs and saw the polling for
frame data and when I pushed the button I saw the click request come through.&lt;/p&gt;

&lt;h2&gt;The Boop Lifecycle&lt;/h2&gt;

&lt;p&gt;At this point I started thinking about how I would create, show and then be done
with a &lt;code&gt;Boop&lt;/code&gt; record. I was thinking of this as a queue. There would be a page
where a button would be clicked to create the &lt;code&gt;Boop&lt;/code&gt; record. Then it would show
up in the frames API call and display on the device. Then I would press the top
button which would send a click API call which would dismiss the &lt;code&gt;Boop&lt;/code&gt; and the
next one off the queue would show up. Wash, rinse and repeat.&lt;/p&gt;

&lt;h2&gt;Look and Feel&lt;/h2&gt;

&lt;p&gt;This lead me to think about how the frames would look on the device. I was
picturing an icon on one side and the text on the other. I wasn&apos;t totally sure
about the text but I thought that it would be fun to pick some icons. LaMetric
provides icons for developers to use and has a &lt;a href=&quot;https://developer.lametric.com/icons&quot;&gt;Gallery Page&lt;/a&gt; so I went
hunting for some good options - here&apos;s what I picked:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-94/boop-icons-full.png&quot;&gt;
    &lt;img alt=&quot;Icon options for Boop&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-94/boop-icons-full.png&quot; srcset=&quot;/images/post-94/boop-icons-900.png 900w, /images/post-94/boop-icons-1200.png 1200w, /images/post-94/boop-icons-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;There are a LOT of icons to choose from but these stood out as pretty nice.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;The Boop Rails Model&lt;/h2&gt;

&lt;p&gt;Given the lifecycle I had in mind and the look and feel that was starting to
solidify in my mind, it was time to start modeling. What fields should be on a
&lt;code&gt;Boop&lt;/code&gt; record? I jotted some notes and ended up with this migration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class CreateBoops &amp;lt; ActiveRecord::Migration[8.0]
  def change
    create_table :boops do |t|
      t.string :display_type, null: false
      t.integer :number, null: false
      t.timestamp :dismissed_at
      t.timestamps
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Putting it all together I made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/281&quot;&gt;Add basic boop lifecycle&lt;/a&gt; and
then &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/282&quot;&gt;Different Boop frame approach&lt;/a&gt; quickly after playing with
the text. I didn&apos;t have a button to click on to create &lt;code&gt;Boop&lt;/code&gt; records so I just
opened a Rails console and did it manually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; Boop.create(display_type: &quot;smile&quot;, number: 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It worked and I started seeing them show up on the device! I pressed the button,
got a satisfying success sound, and then the next one showed up. Here&apos;s how one
looks:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-94/boop-16-full.png&quot;&gt;
    &lt;img alt=&quot;Boop frames&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-94/boop-16-full.png&quot; srcset=&quot;/images/post-94/boop-16-900.png 900w, /images/post-94/boop-16-1200.png 1200w, /images/post-94/boop-16-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;This is a Boop record with display_type of smile and number of 16.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;There&apos;s an animation where one frame slides down on top of the other. It&apos;s Boop
time!!&lt;/p&gt;

&lt;h2&gt;Adding a Public Boop Page&lt;/h2&gt;

&lt;p&gt;The next milestone was to create a public page where people could pick an icon
and send me a Boop. This is a silly project so I had some fun writing up some
copy and then adding some flair.&lt;/p&gt;

&lt;p&gt;There did end up being an interesting technical challenge - the
&lt;code&gt;Boop.display_type&lt;/code&gt; field is stored as a string but I wanted to run the icon
options on the page and have them be backed by a radio button group.&lt;/p&gt;

&lt;p&gt;I tinkered with this for a while and ended up really liking how it came out.
What I did was wrap a &lt;code&gt;label&lt;/code&gt; element around the &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;img&lt;/code&gt; elements for
each option. Then I hid the &lt;code&gt;input&lt;/code&gt; and used some fancy CSS to arrange things
nicely in a single rail with a bottom border. Because the icon images were
inside the &lt;code&gt;label&lt;/code&gt; element when you click on the &lt;code&gt;img&lt;/code&gt; then it selects that
&lt;code&gt;input&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Brief aside to mention a trick that I had not considered. Given this type of
markup structure:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;label&amp;gt;
  &amp;lt;input type=&quot;radio&quot;&amp;gt;
  &amp;lt;img&amp;gt;
&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;How might we highlight the image of the checked option? Turns out it&apos;s easy!&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;:checked + img {
  border-color: pink;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s not exactly what I ended up with but close enough. The trick is knowing
that the pseudo selector &lt;code&gt;:checked&lt;/code&gt; applies here and if we use a plus sign then
the styles will apply to the next element not the checked one. CSS is maddening
to me but when you find the perfect way to do something it is so satisfying!!&lt;/p&gt;

&lt;p&gt;Ok back to &lt;a href=&quot;https://app.jonallured.com/boops&quot;&gt;the public Boop page&lt;/a&gt; - here&apos;s what I ended up building:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-94/public-page-full.png&quot;&gt;
    &lt;img alt=&quot;Public page to send Boop&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-94/public-page-full.png&quot; srcset=&quot;/images/post-94/public-page-900.png 900w, /images/post-94/public-page-1200.png 1200w, /images/post-94/public-page-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;There&apos;s more there - take a look to find out what&apos;s below the fold!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Some Boops are spooky - send me a Boop right now! Please also keep in mind that
I made &lt;a href=&quot;https://github.com/jonallured/shrt/pull/27&quot;&gt;Add redirect to boop&lt;/a&gt; so that you will have a memorable
shortcut: &lt;a href=&quot;https://jon.zone/boop&quot;&gt;jon.zone/boop&lt;/a&gt; so you can Boop anywhere, anytime.&lt;/p&gt;

&lt;h2&gt;Quality of Life Improvements&lt;/h2&gt;

&lt;p&gt;My Rails app has a generator for CRUD pages so it is very easy to add an admin
section:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ bin/rails generator crud:pages Boop
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So I made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/285&quot;&gt;Add Boop CRUD pages&lt;/a&gt; and got that going. I want to
make a generator command for adding API endpoints but haven&apos;t gotten there yet
so I did it manually with &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/286&quot;&gt;Add api endpoints for boop&lt;/a&gt;. I&apos;m
glad I did these 2 PRs because it helped me see a couple areas for improvement
around Rails validations for the &lt;code&gt;Boop&lt;/code&gt; model.&lt;/p&gt;

&lt;h2&gt;Boop from the Terminal&lt;/h2&gt;

&lt;p&gt;With the API endpoints in place it was now possible to Boop right from my
terminal using &lt;a href=&quot;https://httpie.io&quot;&gt;httpie&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ http https://app.jonallured.com/api/v1/boops display_type=skull
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So then I went to my Raspberry Pi and setup a cronjob to Boop me during work
hours on the 7s:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;7 8-16 * * 1-5 http https://app.jonallured.com/api/v1/boops display_type=robot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wait did you notice a new &lt;code&gt;display_type&lt;/code&gt;?? Yep I made &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/288&quot;&gt;Add more Boop display
types&lt;/a&gt; because I felt like an automated Boop should have a
robot icon. While looking for a good one I also found a fun monster one so I
added that too. I didn&apos;t add them to the public page so they are sorta Easter
eggs for you dear reader.&lt;/p&gt;

&lt;h2&gt;Boop with a CLI&lt;/h2&gt;

&lt;p&gt;Don&apos;t you think it should be easier to Boop? No one should have to remember a
URL to create a Boop. I already had a solution - my CLI Ruby gem that wraps the
Monolithium API called &lt;a href=&quot;https://github.com/jonallured/mli&quot;&gt;mli&lt;/a&gt;. What I wanted was this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ mli boops create --display-type monster
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I made &lt;a href=&quot;https://github.com/jonallured/mli/pull/17&quot;&gt;Add Boop command and resource&lt;/a&gt; and had that done. Installing
that project is done by cloning the repo and then running &lt;code&gt;rake install&lt;/code&gt; so
there&apos;s a bit of friction but boy is it easy to Boop now!&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I want to thank Shopify for buying me this fun TIME smart clock and for
providing the Hackathon time to play with it. I had a blast stepping away from
my normal work to do something completely different. My work is so digital that
when I can do something with hardware that manifests itself in the real world I
just have to do it.&lt;/p&gt;

&lt;p&gt;And I hope this project made you smile. Maybe feel some whimsy? Things are very
challenging in the real world right now so having some fun was a wonderful
distraction. Keep those Boops coming!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Week in Review: Week 6, 2026</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/02/08/week-in-review-week-6-2026.html"/>
    <id>https://www.jonallured.com/posts/2026/02/08/week-in-review-week-6-2026.html</id>
    <published>2026-02-08T15:33:00-06:00</published>
    <updated>2026-02-08T15:33:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;There were parent/teacher conferences this week so the boy was off school
Thursday and Friday. He&apos;s 10 so not too bad but I just found it hard to focus.
We went to the gym on Friday and hit the pool which was pretty fun. He likes to
put on the flippers they let kids use and race me. He always wins!&lt;/p&gt;

&lt;p&gt;Minnesota is still a mess despite what the media or the White House might be
saying. As a tiny bit of positive news though, I will tell you that I got hooked
up with an organization that helps get food to people that are too scared to
leave their house. This was the second week where I got a grocery list, went
shopping, and then dropped the food at this organization. Then other volunteers
collected it for delivery to these families.&lt;/p&gt;

&lt;p&gt;I mean I&apos;m not protesting in the streets but this did feel really good. Very
direct help to those in need. But. BUT. This is not normal and we should not
rest until ICE is abolished.&lt;/p&gt;

&lt;h2&gt;Highlights&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;worked 40:30, no PTO&lt;/li&gt;
  &lt;li&gt;published &lt;a href=&quot;https://www.buzzsprout.com/1470301/episodes/18627646-new-old&quot;&gt;New Old&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;created &lt;a href=&quot;https://github.com/search?order=asc&amp;amp;q=author%3A%40me+created%3A2026-02-01..2026-02-07+type%3Apr&amp;amp;sort=created&amp;amp;type=issues&quot;&gt;9 Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent most of my time for personal projects tinkering with my CLI tools. This
work is both fun and also annoying. My love of Ruby and boring tools has lead me
to use &lt;a href=&quot;https://github.com/rails/thor&quot;&gt;Thor&lt;/a&gt; as the foundation for these tools and wow is it painful to work
with. I&apos;m getting there though - almost have some patterns I don&apos;t hate.&lt;/p&gt;

&lt;h2&gt;Some Best Things&lt;/h2&gt;

&lt;p&gt;Are you playing &lt;a href=&quot;https://enclose.horse&quot;&gt;Enclose Horse&lt;/a&gt; yet? You should really be playing that game
every day - I usually play while I&apos;m drinking my coffee in the morning. Watch
out for bees! I guess h/t to &lt;a href=&quot;https://bio.link/erikk&quot;&gt;Erik Krietsch&lt;/a&gt; but he knows what he did to
me and my free time.&lt;/p&gt;

&lt;h2&gt;Superb Owl Prediction&lt;/h2&gt;

&lt;p&gt;My mind tells me that the Patriots will win yet again but my heart hopes both
teams lose somehow. I suppose I&apos;ll cheer for the Seahawks. Official prediction
for bragging rights in case I&apos;m right: SEA 17 / NE 24.&lt;/p&gt;

&lt;h2&gt;Next Week&lt;/h2&gt;

&lt;p&gt;You are running out of time to get your Valentine&apos;s Day card and flowers
arranged. And by &quot;you&quot; I mean &quot;me&quot;. Gotta do that tomorrow for sure. Other than
that I have a chill week it looks like. I expect to do another grocery store run
for some needy families so looking forward to that. Oh and I really do want this
to be the week I upgrade my Ruby projects to 4 - it&apos;s time.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Week in Review: Week 4, 2026</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/01/25/week-in-review-week-4-2026.html"/>
    <id>https://www.jonallured.com/posts/2026/01/25/week-in-review-week-4-2026.html</id>
    <published>2026-01-25T16:43:00-06:00</published>
    <updated>2026-01-25T16:43:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;This was a weird week on many levels. Monday was a holiday but then people were
talking about a general strike for Friday. No work, no school, no shopping. I
wasn&apos;t sure what I would do but then at the same time an insane cold front came
through. The high for Friday was predicted to be -10 F which was enough for
schools to close.&lt;/p&gt;

&lt;p&gt;Since Jess and Jack were going to be home and the strike was on I decided to
observe it as well. We basically camped out at home and rested up trying to stay
warm. We watched some coverage of the strike and talked to Jack about why we
were striking and what it meant. We talked about being proud of how the people
of our state were responding to the situation. He&apos;s 10 so we did our best.&lt;/p&gt;

&lt;p&gt;Then the very next day ICE agents murdered another Minnesota man. Crushed,
pissed, scared, powerless. Lots of feelings. What do I tell my son now?&lt;/p&gt;

&lt;h2&gt;Highlights&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;worked 24:00, 16:00 PTO&lt;/li&gt;
  &lt;li&gt;created &lt;a href=&quot;https://github.com/search?order=asc&amp;amp;q=author%3A%40me+created%3A2026-01-18..2026-01-24+type%3Apr&amp;amp;sort=created&amp;amp;type=issues&quot;&gt;10 Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of my time was spent exploring how I will automate the task of announcing
on social media when I pushing a blog post. I have mostly got this working
locally for Bluesky but nothing I&apos;m ready to actually ship just yet. And I want
to get into Mastodon&apos;s API too so I can get that automated too.&lt;/p&gt;

&lt;h2&gt;Some Best Things&lt;/h2&gt;

&lt;p&gt;I&apos;m trying out a new section - let me know what you think. The idea is to just
share some of the best things from the week. Here goes.&lt;/p&gt;

&lt;p&gt;Some fun new music came out this week. My new favorite band The Barbarians of
California released &lt;a href=&quot;https://www.youtube.com/watch?v=09_-B07cwww&quot;&gt;Bomb To A Knife Fight&lt;/a&gt;. In Apple Music it is on a
&quot;single&quot; with a couple other songs that I also really like. A couple days later
Pelican released an EP called &lt;a href=&quot;https://pelican.bandcamp.com/album/ascending-ep&quot;&gt;Ascending&lt;/a&gt;. They are an instrumental metal band
but they did a track with &lt;a href=&quot;https://en.wikipedia.org/wiki/Geoff_Rickly&quot;&gt;Geoff Rickly&lt;/a&gt; which is kinda rad.&lt;/p&gt;

&lt;p&gt;I was listening to &lt;a href=&quot;https://www.podpage.com/mike-birbiglias-working-it-out/199-jordan-jensen-you-cant-say-that-but-actually-you-can/&quot;&gt;episode 199&lt;/a&gt; of Working it Out and the guest was
Jordan Jensen. They talked about her new special and that reminded me that I had
noticed it but not watched it yet. She was so funny and cool on the podcast
episode that I paused it and then later that night I watched &lt;a href=&quot;https://www.netflix.com/title/81978275&quot;&gt;Take Me With
You&lt;/a&gt; and it was also very funny and cool.&lt;/p&gt;

&lt;p&gt;A bunch of us went to the Timberwolves game on Thursday and they were playing
the Bulls. I grew up loving the Bulls and so it was really rewarding to watch
them actually pull off an upset and win! This win got them to .500 which…is
better than nothing.&lt;/p&gt;

&lt;h2&gt;Next Week&lt;/h2&gt;

&lt;p&gt;We have a friend in town so I&apos;m excited to see him for a dinner date. There are
also a few other family things including a band concert so you know just a
wholesome Midwestern week coming up. Other than that I can&apos;t think of anything
really going on in my area - you?&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Week in Review: Week 3, 2026</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/01/18/week-in-review-week-3-2026.html"/>
    <id>https://www.jonallured.com/posts/2026/01/18/week-in-review-week-3-2026.html</id>
    <published>2026-01-18T13:20:00-06:00</published>
    <updated>2026-01-18T13:20:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;This was another hard week to be living in Minnesota. Don&apos;t listen to what the
White House is telling you - this ICE activity has nothing to do with fraud and
everything to do with terrorizing a community for political reasons. Hard to
stay positive but sometimes channeling my feelings into working on my personal
projects provides some relief. I did that a lot this week.&lt;/p&gt;

&lt;h2&gt;Highlights&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;worked 40:00, no PTO&lt;/li&gt;
  &lt;li&gt;published &lt;a href=&quot;https://puddingtime.buzzsprout.com/1470301/episodes/18489215-blog-balm&quot;&gt;Blog Balm&lt;/a&gt; for Pudding Time&lt;/li&gt;
  &lt;li&gt;published &lt;a href=&quot;https://www.jonallured.com/posts/2026/01/12/configure-spec-task-without-verbose-output.html&quot;&gt;Configure Spec Task Without Verbose Output&lt;/a&gt; on here&lt;/li&gt;
  &lt;li&gt;published &lt;a href=&quot;https://www.jonallured.com/posts/2026/01/17/building-analytics-reports-in-rails-using-apache-logs.html&quot;&gt;Building Analytics Reports in Rails Using Apache Logs&lt;/a&gt; on
here&lt;/li&gt;
  &lt;li&gt;created &lt;a href=&quot;https://github.com/search?order=asc&amp;amp;q=author%3A%40me+created%3A2026-01-11..2026-01-17+type%3Apr&amp;amp;sort=created&amp;amp;type=issues&quot;&gt;23 Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest thing was landing the work to create an Analytics section on my
Rails app for this website. That blog post goes into great detail but I&apos;ll just
say that it feels really good to have that done and shipped. I did have to fix
something today because I wasn&apos;t correctly managing the new access log data so
I&apos;ll see if that fix worked tomorrow.&lt;/p&gt;

&lt;h2&gt;Pic of the Week&lt;/h2&gt;

&lt;p&gt;On Friday the Marvel Rivals game started a new season and now you can play as
Deadpool. When Jack got home from school he got some time to try him out and he
really likes the new character:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-91/jack-jump-full.jpg&quot;&gt;
    &lt;img alt=&quot;Jack jumping&quot; height=&quot;570&quot; loading=&quot;eager&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-91/jack-jump-full.jpg&quot; srcset=&quot;/images/post-91/jack-jump-900.jpg 900w, /images/post-91/jack-jump-1200.jpg 1200w, /images/post-91/jack-jump-1800.jpg 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;You would think jumping like this would mess him up.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Next Week&lt;/h2&gt;

&lt;p&gt;Monday is a holiday here in the US - Martin Luther King Jr. Day and so I have
that day off work. Jess and Jack have it off school too so we will all be home
pondering how this country has fallen so low. There&apos;s talk of a protest day on
Friday and I&apos;m keeping an eye on that.&lt;/p&gt;

&lt;p&gt;In terms of projects - maybe on Monday I&apos;ll find some motivation to build
something. It might be a good time to upgrade to Ruby 4. I&apos;ll also be monitoring
my fancy new ETL pipeline to make sure my Analytics section is working
correctly.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Building Analytics Reports in Rails Using Apache Logs</title>
    <link rel="alternate" href="https://www.jonallured.com/posts/2026/01/17/building-analytics-reports-in-rails-using-apache-logs.html"/>
    <id>https://www.jonallured.com/posts/2026/01/17/building-analytics-reports-in-rails-using-apache-logs.html</id>
    <published>2026-01-17T16:42:00-06:00</published>
    <updated>2026-01-17T16:42:00-06:00</updated>
    <author>
      <name>Jon Allured</name>
    </author>
    <content type="html">&lt;p&gt;Over on &lt;a href=&quot;https://www.jonallured.com/posts/2025/10/29/evaluating-apache-access-log-data.html&quot;&gt;Evaluating Apache Access Log Data&lt;/a&gt; I dove deep on the data
available to me from my many years of hoarding Apache access log data. By
working with the logs in this way I learned what data points existed. I got
familiar with the patterns and saw a lot of junk. That made it possible for me
to hone in on what exactly I cared about.&lt;/p&gt;

&lt;p&gt;The end goal was to create a section on my Rails app for website analytics
reports. I broke up this work into 4 PRs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272&quot;&gt;Import Apache log files with ETL pipeline&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/274&quot;&gt;View analytics reports with Apache log data&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/275&quot;&gt;Add CRUD pages for analytics models&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/276&quot;&gt;Configure recurring import process for analytics data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post will cover these PRs and detail my approach plus any interesting
things I learned.&lt;/p&gt;

&lt;h2&gt;Import Apache log files with ETL pipeline&lt;/h2&gt;

&lt;p&gt;The first PR of the set was all about getting the modeling right and then using
the concept of an ETL pipeline to import the Apache data from S3. I added rake
tasks to work on this locally and then also with my production deploy at Heroku.&lt;/p&gt;

&lt;h3&gt;Adding a parent model&lt;/h3&gt;

&lt;p&gt;Something that popped out of this process was splitting up the modeling into a
parent/child relationship:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;ApacheLogFile&lt;/code&gt;: represents a daily file of requests&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;ApacheLogItem&lt;/code&gt;: represents an individual request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An &lt;code&gt;ApacheLogFile&lt;/code&gt; record has many &lt;code&gt;ApacheLogItem&lt;/code&gt; records. The &lt;code&gt;ApacheLogFile&lt;/code&gt;
is where I store the content of the access log text files and the
&lt;code&gt;ApacheLogItem&lt;/code&gt; is where I break that text down into lines and create one for
each of them. Well that&apos;s not exactly true but more on that later.&lt;/p&gt;

&lt;p&gt;Making this modeling choice had a really nice benefit which is that I could
create the &lt;code&gt;ApacheLogFile&lt;/code&gt; records and download the Apache log data as my
extract step but then the transform and loading could happen separately. Prior
to this modeling choice my approach was more like Extract-Load-Transform and was
clunky.&lt;/p&gt;

&lt;p&gt;If you want the juicy details this is the commit to check out: &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272/commits/d23758add8b9889fc11e200b6f0f8a845318a460&quot;&gt;d23758a&lt;/a&gt;. It&apos;s
where I have the database migrations so you can see all the various fields.&lt;/p&gt;

&lt;h3&gt;Associated Objects for ETL Classes&lt;/h3&gt;

&lt;p&gt;The next 3 commits on this PR are actually very readable. They each take a
letter in ETL and create an &lt;a href=&quot;https://github.com/kaspth/active_record-associated_object&quot;&gt;Associated Object&lt;/a&gt; for it. Side note: you
should totally check out this gem if you haven&apos;t seen it yet - it&apos;s a great way
to organize Rails code!&lt;/p&gt;

&lt;p&gt;Anyway here are the next 3 commits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272/commits/0b777000e85a6fc47523b520dc9d64fafe899a29&quot;&gt;Extract apache log data from S3&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272/commits/973d22417e274b62587715ba04c1aa3db82ba20c&quot;&gt;Transform raw apache log lines&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272/commits/18306c809588771a825846b8a37ae3857a55a696&quot;&gt;Load parsed apache log entries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Extractor Class&lt;/h3&gt;

&lt;p&gt;The Extractor class is simple: use the &lt;code&gt;dateext&lt;/code&gt; value to construct the S3 key
and then grab the data. Unzip it and then update the given &lt;code&gt;ApacheLogFile&lt;/code&gt;
record we are working with.&lt;/p&gt;

&lt;h3&gt;Transformer Class&lt;/h3&gt;

&lt;p&gt;The Transformer class is bit more complex but mostly because it is where I am
mapping the individual access log lines to the database columns where it will
ultimately be stored. I organized it as parse, then normalize. So parsing is
where the regex happens with a bit of logic about how to handle weird parse
results. Then normalization is where I can massage things so that there is
consistency.&lt;/p&gt;

&lt;p&gt;This class ends up taking an array of strings (the log lines) and transforming
each into a hash. This is stored back on the &lt;code&gt;ApacheLogFile&lt;/code&gt; in a jsonb column.&lt;/p&gt;

&lt;h3&gt;Loader Class&lt;/h3&gt;

&lt;p&gt;We are now ready to take the parsed log lines and turn them into &lt;code&gt;ApacheLogItem&lt;/code&gt;
records but only if they are worth importing. As I said at the top, I spent a
lot of time evaluating this data and pretty quickly I realized that my server
receives a LOT of garbage requests. This resulted in me building up a set of
rules that I wanted to use to eliminate the irrelevant requests so that I would
be left with only requests that were interesting to look at.&lt;/p&gt;

&lt;p&gt;In Rails terms I decided to formulate these rules as validation. I started off
writing them right in the &lt;code&gt;ApacheLogItem&lt;/code&gt; class but soon realized that I&apos;d want
something better so I checked &lt;a href=&quot;https://guides.rubyonrails.org/active_record_validations.html&quot;&gt;the docs&lt;/a&gt; for my options and that
reminded me that you can validate a Rails model with a class that inherits from
&lt;code&gt;ActiveModel::Validator&lt;/code&gt; so that&apos;s what I did in this commit.&lt;/p&gt;

&lt;p&gt;The Loader class then becomes very simple - take the parsed data from the
&lt;code&gt;ApacheLogFile&lt;/code&gt; record, use it to create an &lt;code&gt;ApacheLogItem&lt;/code&gt; record and validate
it. Only persist the valid ones and then we are done.&lt;/p&gt;

&lt;p&gt;I really enjoyed the process of writing this commit because there was a tight
TDD loop where I would take my &quot;rules&quot; and break them down into test cases and
then just knock them off one-by-one.&lt;/p&gt;

&lt;h3&gt;Importing data&lt;/h3&gt;

&lt;p&gt;Now that I had each step of the ETL pipeline defined I needed just a bit of
orchestration. What I wanted was to be able to run a Rake task and have it
enqueue background jobs that would call a method to kick off the import process.&lt;/p&gt;

&lt;p&gt;That final bit looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class ApacheLogFile &amp;lt; ApplicationRecord
  def self.import(dateext)
    apache_log_file = create(dateext: dateext, state: &quot;pending&quot;)
    apache_log_file.extractor.run
    apache_log_file.transformer.run
    apache_log_file.loader.run
    apache_log_file
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s just a snippet of the model file but yeah I really like how that method
turned out. The ETL import pipeline is defined as creating an &lt;code&gt;ApacheLogFile&lt;/code&gt;
record and then calling &lt;code&gt;run&lt;/code&gt; on each step of the process - super easy to see
how it all fits together.&lt;/p&gt;

&lt;p&gt;Bubbling back up to my overall goals then it was clear that I&apos;d want 2 tasks -
one to load an individual file and one to backfill. I&apos;d use the former locally
to iterate on this process and then check that everything can be imported
correctly with the backfill task. Those are invoked like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ bundle exec rake apache_logs:backfill
$ bundle exec rake apache_logs:import[20251201]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The former would slurp up the entire set of Apache log files on S3 but the
latter would just grab the one that matched the &lt;code&gt;dateext&lt;/code&gt; that I passed in. Tiny
aside: &lt;code&gt;dateext&lt;/code&gt; is the &lt;a href=&quot;https://github.com/logrotate/logrotate&quot;&gt;logrotate&lt;/a&gt; term for the way that it puts a datestamp
into the filename like &lt;code&gt;access.log-20251201.gz&lt;/code&gt; so I followed that terminology
for better or worse.&lt;/p&gt;

&lt;h3&gt;Running the import locally&lt;/h3&gt;

&lt;p&gt;Prior to the addition of the &lt;code&gt;ApacheLogFile&lt;/code&gt; model I would create an
&lt;code&gt;ApacheLogItem&lt;/code&gt; record for &lt;em&gt;every&lt;/em&gt; line in the access log data. That created
millions of records that I ended up culling down to less than a hundred thousand
as I learned more. This is why it was a better to extract everything into the
&lt;code&gt;ApacheLogFile&lt;/code&gt; record, transform the data there and only actually load the
records that I wanted to keep.&lt;/p&gt;

&lt;p&gt;When I first started working with the data it would take 3 or more hours to
import everything but with this better modeling it was down to 10 minutes.&lt;/p&gt;

&lt;h3&gt;Running the import on Heroku&lt;/h3&gt;

&lt;p&gt;Once the PR was merged and deployed to Heroku I kicked off the backfill and it
was quite an adventure! Turns out my Heroku worker dynos were running out of
memory and it caused all sorts of problems. I detailed this in &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/272#issuecomment-3762219604&quot;&gt;a
comment&lt;/a&gt; if you are interested in the journey but the solution was
a combination of changing the thread count to 1 and just restarting the dynos
when they were crashed.&lt;/p&gt;

&lt;p&gt;I did have to do some data cleanup but it wasn&apos;t too bad and ultimately the
actual ETL pipeline was all correct - the only issues were more like the
constraints of Heroku not the code.&lt;/p&gt;

&lt;h2&gt;View analytics reports with Apache log data&lt;/h2&gt;

&lt;p&gt;Major milestone reached! The Apache access log data was now sitting on Heroku
and ready to be viewed. All my careful tinkering with the ETL code was correct.
PHEW. But this data doesn&apos;t do much unless we have a way to look at it.&lt;/p&gt;

&lt;h3&gt;Start With Sketching&lt;/h3&gt;

&lt;p&gt;When you aren&apos;t sure what to build then a great place to start is with some
sketching. I grabbed some paper and a pen and sketched out a few things. I drew
in very broad strokes just to get some ideas flowing. As I went I also scribbled
some notes about the way the reports might work. Param names and values - things
like that.&lt;/p&gt;

&lt;p&gt;I quickly landed on a page design that used some text labels to indicate the
month and year of the report followed by a table of…something.&lt;/p&gt;

&lt;p&gt;This sketch explores setting the metric/mode of the report:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/sketch-1-full.jpg&quot;&gt;
    &lt;img alt=&quot;Rough sketch of analytics report&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/sketch-1-full.jpg&quot; srcset=&quot;/images/post-90/sketch-1-900.jpg 900w, /images/post-90/sketch-1-1200.jpg 1200w, /images/post-90/sketch-1-1800.jpg 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;I find that even very rough sketching like this helps me zero in on what I&apos;m building.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This sketch explores navigating through periods:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/sketch-2-full.jpg&quot;&gt;
    &lt;img alt=&quot;Rough sketch of report with navigation&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/sketch-2-full.jpg&quot; srcset=&quot;/images/post-90/sketch-2-900.jpg 900w, /images/post-90/sketch-2-1200.jpg 1200w, /images/post-90/sketch-2-1800.jpg 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;There are other pages that navigate between periods with these types of links so I hoped I could reuse code from them.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3&gt;URLs as UI&lt;/h3&gt;

&lt;p&gt;Before I knew it I was starting to look at my sketches and ponder what the URLs
would look like. I moved to writing out URL options and iterated a bunch. I
actually filled up a few pages with different approaches to organizing things
via URL parts. This was extremely helpful in focusing me on what I wanted to
build.&lt;/p&gt;

&lt;p&gt;Here&apos;s where I ended up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# Rails route
/analytics/:metric/:mode/:year/:month
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I knew I had nailed it when I could look at a given URL and translate it into
English. Here are couple examples:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/analytics/browsers/summary/2021/01
=&amp;gt; summary view of browser names during January 2021

/analytics/pages/detail/2025/11
=&amp;gt; detail view of pages requested during November 2025
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point I put paper and pen down and started coding with some enthusiasm
because I had the beginnings of a plan.&lt;/p&gt;

&lt;h3&gt;Metrics by Modes in Periods&lt;/h3&gt;

&lt;p&gt;The plan was to build a page where I could pick a metric to view by a mode with
matching records in a given period. To start I&apos;m focusing on metrics I either
already had or could easily get:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Browser - take the user agent value and parse it with the &lt;a href=&quot;https://github.com/fnando/browser&quot;&gt;browser&lt;/a&gt; gem&lt;/li&gt;
  &lt;li&gt;Page - use the page request value straight from the log&lt;/li&gt;
  &lt;li&gt;Referrer - use the header value straight from the log&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For modes I was thinking of a summary that would group and count items plus a
detail view in case I wanted to look at individual log items.&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/initial-look-full.png&quot;&gt;
    &lt;img alt=&quot;Initial look of analytics report&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/initial-look-full.png&quot; srcset=&quot;/images/post-90/initial-look-900.png 900w, /images/post-90/initial-look-1200.png 1200w, /images/post-90/initial-look-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;I like reports that use a sentence to describe what data they are showing.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;At this point I felt like I had explored the idea enough to have some thoughts
about what I wanted to do - spike complete. Time to head back to the main
branch, start a system spec, and pivot to making this for real. That&apos;s what &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/274&quot;&gt;the
next PR&lt;/a&gt; does.&lt;/p&gt;

&lt;h3&gt;Reporting Classes&lt;/h3&gt;

&lt;p&gt;The only &lt;em&gt;maybe&lt;/em&gt; interesting thing I&apos;ll call out is that I ended up building
this out with page objects. Here&apos;s the lineup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Summary Report - uses &lt;code&gt;Analytics::SummaryReport&lt;/code&gt; and &lt;code&gt;Analytics::SummaryRow&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Detail Report - uses &lt;code&gt;Analytics::DetailReport&lt;/code&gt; and &lt;code&gt;Analytics::DetailRow&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I extracted an &lt;code&gt;Analytics::BaseReport&lt;/code&gt; class as I went and it was nice to see
how using POROs made this code easy to land. I deployed this to Heroku and
clicked around and the pages loaded great with no performance issues at all.
Something was actually easy!&lt;/p&gt;

&lt;h2&gt;Add CRUD pages for analytics models&lt;/h2&gt;

&lt;p&gt;Next on my list was to do a little bit of paperwork. A while back I created a
Rails generator that will take a model and create the CRUD pages that can be
used to admin those records. I don&apos;t use these admin pages all that often but I
do like making them just in case it&apos;s handy to be able to use a UI to tinker
with things. I actually did wire up the detail report to link to the show page
so if I see something off in the reports then I can jump right to a view of the
record.&lt;/p&gt;

&lt;p&gt;Here&apos;s the list page:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/crud-list-page-full.png&quot;&gt;
    &lt;img alt=&quot;List of ApacheLogItem records&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/crud-list-page-full.png&quot; srcset=&quot;/images/post-90/crud-list-page-900.png 900w, /images/post-90/crud-list-page-1200.png 1200w, /images/post-90/crud-list-page-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;I should have added something like dateext and maybe line number to make this more useful - oh well.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And here&apos;s the detail page:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/crud-detail-page-full.png&quot;&gt;
    &lt;img alt=&quot;Detail of ApacheLogItem record&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/crud-detail-page-full.png&quot; srcset=&quot;/images/post-90/crud-detail-page-900.png 900w, /images/post-90/crud-detail-page-1200.png 1200w, /images/post-90/crud-detail-page-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;I keep these CRUD pages shallow rather than nesting them because it is more flexible.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;To generate these pages I ran these commands:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ bin/rails generate crud:pages ApacheLogFile
$ bin/rails generate crud:pages ApacheLogItem
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From there it&apos;s just a matter of filling out some &lt;code&gt;REPLACE_ME&lt;/code&gt; details and
getting the generated specs to pass. &lt;a href=&quot;https://github.com/jonallured/monolithium/pull/275&quot;&gt;The PR&lt;/a&gt; alternates between these
two steps.&lt;/p&gt;

&lt;p&gt;I do need to write up a post about my generator - so much to do so little time.&lt;/p&gt;

&lt;h2&gt;Configure recurring import process for analytics data&lt;/h2&gt;

&lt;p&gt;The last part of this that needed to be done was configuring a nightly job to
import whatever new data had been uploaded to S3. I already had a few jobs setup
in my &lt;code&gt;config/recurring.yml&lt;/code&gt; so adding another would be easy.&lt;/p&gt;

&lt;p&gt;What I had in mind was an update to the Extract class that would move files into
an &quot;archive&quot; folder. The way that logrotate works is that each day has an
&lt;code&gt;access.log&lt;/code&gt; and an &lt;code&gt;error.log&lt;/code&gt; file so here&apos;s what I was thinking:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# before
domino/logs/access.log-20251201.gz
domino/logs/errors.log-20251201.gz

# after
domino/archives/access.log-20251201.gz
domino/archives/errors.log-20251201.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Take each day&apos;s files that I had already imported and just move them from &quot;logs&quot;
to &quot;archives&quot;. Then list what remained in the &quot;logs&quot; folder each night and it
would only be the new day&apos;s files. Archive those too and we have a recurring
import process.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/jonallured/monolithium/pull/276&quot;&gt;The PR&lt;/a&gt; is pretty straightforward but I did have to do a bit of
trickery when running it over the records that were already imported:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; ApacheLogFile.all.each { it.extractor.send(:archive_files) }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When I attempted this in a Rails console in production the Heroku dyno crashed.
I&apos;m not totally sure why - I even tried again with &lt;code&gt;; nil&lt;/code&gt; at the end in case it
was trying to return the records. Rather than spend any further time fighting
with out of memory dynos I just ran it on my laptop and called that good enough.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href=&quot;https://panic.com/transmit/&quot;&gt;Transmit&lt;/a&gt; app to view my S3 buckets so here&apos;s what it looked like
before:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/transmit-before-full.png&quot;&gt;
    &lt;img alt=&quot;Transmit list of files&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/transmit-before-full.png&quot; srcset=&quot;/images/post-90/transmit-before-900.png 900w, /images/post-90/transmit-before-1200.png 1200w, /images/post-90/transmit-before-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Please pay no attention to the left hand side where you can see my home folder listing.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And then here&apos;s what it looked like after I ran the above command and did a
little extra random cleanup:&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;/images/post-90/transmit-after-full.png&quot;&gt;
    &lt;img alt=&quot;Cleaned list of files&quot; height=&quot;570&quot; loading=&quot;lazy&quot; sizes=&quot;(max-width: 800px) calc(100vw - 80px), 760px&quot; src=&quot;/images/post-90/transmit-after-full.png&quot; srcset=&quot;/images/post-90/transmit-after-900.png 900w, /images/post-90/transmit-after-1200.png 1200w, /images/post-90/transmit-after-1800.png 1800w&quot; title=&quot;click for bigger&quot; width=&quot;760&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;I wonder why I have a folder called javasharedresources and what would happen if I removed it.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And that is correct - those 2 remaining files were not imported yet and should
be picked up tonight. Along with whatever lands in there for today.&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I&apos;ve been working on this project since October when I migrated my blog to
Jekyll and started posting again more seriously. I had no idea it would take
this long! I also did not know that this project would include all these
interesting little details and learnings. Now that this is landed I have some
other ideas for ways I can use this data but I&apos;m just really happy with how it
turned out.&lt;/p&gt;

&lt;p&gt;I also feel really validated that I can get website analytics info for my
personal site with Apache access logs and not use Google Analytics. Sure, I
don&apos;t have as much analytic data but I have enough. I know roughly how many
requests my site gets, which browsers people are using, and where that traffic
is coming from. And I get all this without having to include Javascript in my
site nor having to expose myself and my readers to the privacy destroying
machine that is Google. Please clap!&lt;/p&gt;

</content>
  </entry>
  
</feed>
