{"id":492,"date":"2026-08-15T11:29:26","date_gmt":"2026-08-15T11:29:26","guid":{"rendered":"https:\/\/nazeem.me\/?p=492"},"modified":"2026-08-15T12:13:26","modified_gmt":"2026-08-15T12:13:26","slug":"i-built-a-dashboard-for-the-things-i-actually-check","status":"publish","type":"post","link":"https:\/\/nazeem.me\/?p=492","title":{"rendered":"I Built a Dashboard for the Things I Actually Check"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Five separate logins became one page. The interesting part was how often I turned out to be wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I check five things regularly: whether the homelab is healthy, my portfolio, my health trend, whatever trip is coming up, and my to-do list. Each one lived somewhere different. Each had its own login. None of them talked to each other. I have to go looking for information by opening a browser, going to my bookmarks page and finding the right tile to click:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"854\" src=\"https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927-1024x854.png\" alt=\"My Heimdall Dashboard of links to my self-hosted services.\" class=\"wp-image-493\" srcset=\"https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927-1024x854.png 1024w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927-300x250.png 300w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927-768x641.png 768w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927-1536x1281.png 1536w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_191927.png 1772w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Information you have to go and fetch is information you check less often than you should.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What It Looks Like Now<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One page. Five modules, each running in its own container, pulled together by a dashboard shell called <strong>Glance<\/strong>. The shell is configuration, not a product \u2014 it reads each service&#8217;s API and renders the numbers. That&#8217;s the whole trick (admittedly greatly helped by LLMs to design):<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"936\" height=\"905\" src=\"https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_192114-1.png\" alt=\"The architecture for my data dashboard\" class=\"wp-image-495\" srcset=\"https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_192114-1.png 936w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_192114-1-300x290.png 300w, https:\/\/nazeem.me\/wp-content\/uploads\/2026\/08\/Screenshot_20260815_192114-1-768x743.png 768w\" sizes=\"auto, (max-width: 936px) 100vw, 936px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The health module was the biggest piece by some distance. I exported four years of <strong>Fitbit<\/strong> history and imported it alongside live <strong>Garmin<\/strong> data, so the trend line runs continuously from March 2022 to today instead of restarting when I changed watches. About 7 million data points. There&#8217;s a real gap of six days in January where neither device recorded anything, which is the week I switched. I&#8217;ve left it in. It&#8217;s honest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The finance module pulls positions from my broker and reconciles them against manually entered accounts for the things a broker doesn&#8217;t know about \u2014 Provident Fund, cash, the rest. Everything now sits behind a single login.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Three of My Four API Guesses Were Wrong<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the part worth writing down.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I planned the whole thing on paper first (with the help of AI, of course, including this post), including which endpoint each module would call and which fields it would read. When I came to build it, three of the four were wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The monitoring tool returned a flat structure where I&#8217;d assumed a nested one (I needed the LLM to explain this to me too!). The finance app had moved its performance endpoint to a new API version, and the old path returned a <strong>404<\/strong>. The travel app used different field names to what I was expecting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">None of these would have crashed anything. Each would have quietly rendered an error tile on a dashboard I&#8217;d then have trusted. The fix was boring and it worked: read a live response from every endpoint before writing a single line of template code.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Service I Decided Not to Build<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The last gap was authentication. Several services were reachable on my network with no login at all, and the obvious answer was to put single sign-on in front of everything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So I drafted the whole build. Container, secrets, rollout order, backups. Then I actually tested my assumptions and found that most of the services I&#8217;d listed as unprotected were, in fact, protected. The real list was less than half of what I&#8217;d written down (i.e. in my issue backlog in my Obsidian Vault). A lot of the services were things I wanted open so I could use them on the fly on an ipad or my phone. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I closed the gap with tools already running instead. I stopped proxying my router&#8217;s admin interface entirely rather than putting a login in front of it, and turned off open registration on one app that had it. That took an afternoon and added nothing new to maintain.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A new service is a permanent cost. Backups, updates, secrets to store, another thing that breaks at 2am. Since I already stayed up late last night to figure out why Jellyfin was transcoding when no one was using it, I figured &#8220;<a href=\"https:\/\/en.wikipedia.org\/wiki\/KISS_principle\" data-type=\"link\" data-id=\"https:\/\/en.wikipedia.org\/wiki\/KISS_principle\">Simplex sigillum veri<\/a>&#8220;.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What I&#8217;d Tell Someone Starting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Verify before you build.<\/strong> Every assumption I wrote in my .md vault was cheap to check and expensive to get wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Back up the databases properly.<\/strong> A snapshot of a running database is a snapshot of a database mid-write. Every module here dumps itself fifteen minutes before the nightly backup, and each dump refuses to complete unless it passes an integrity check. A backup you haven&#8217;t verified is a hope.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And scope the problem before you scope the solution. I nearly built an identity provider to solve a problem that turned out to be four services and a router.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Five separate logins became one page. The interesting part was how often I turned out to be wrong.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ai_generated_summary":"","wpai_meta_description":"","footnotes":""},"categories":[5],"tags":[],"class_list":["post-492","post","type-post","status-publish","format-standard","hentry","category-homelab"],"_links":{"self":[{"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/posts\/492","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nazeem.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=492"}],"version-history":[{"count":1,"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/posts\/492\/revisions"}],"predecessor-version":[{"id":496,"href":"https:\/\/nazeem.me\/index.php?rest_route=\/wp\/v2\/posts\/492\/revisions\/496"}],"wp:attachment":[{"href":"https:\/\/nazeem.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=492"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nazeem.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=492"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nazeem.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=492"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}