Four dashboards meant four tabs
YouTube, SNS, Google Search Console, traffic. Four dashboards, each built separately whenever I needed it. Each one worked fine on its own. The problem was that answering "what is the overall state right now" meant opening four places in turn.
So on 2026-08-04 I built a hub: one page that summarizes all four and links out to each source. The commit is hub.py at 159 lines plus one line in .gitignore. That is the whole thing.
One question before going further. How many numbers in your project can only be seen by opening N different places?
When I tried to merge them, half of them could not talk
To put four sources on one page, the hub has to read numbers from each. The four turned out to be in very different shape.
Two were live services. Append ?format=json to the URL and they return JSON. The hub fetches, picks out fields, done.
The other two were static reports. Some job runs and writes a file. There is nothing to ask. All the hub could learn was two things: the file's modification time (mtime) for "how old is this number", and a regex over the body for "what is the number".
What would you do? Add JSON endpoints to the two static sources first and then build the hub, or scrape them with a regex and move on?
What I chose and what it cost
I scraped and moved on. Two live sources via JSON, two static sources via mtime plus regex. One summary page, four links. It binds to the tailnet IP on port 8888, so it is visible only to my own devices, not the public internet.
I still think that was the shortest path. But I should be explicit about what the choice paid for.
A regex is tied to the report format. If the job that writes the report changes a phrase or reorders a line, the hub does not throw. It either fails to find the number or finds the wrong one. And mtime only says "this file was written again". It does not say the number inside is correct, or that the job did not fail halfway and leave an empty file behind.
So the hub puts four sources on one screen, but half of them are closer to "scraped and guessed" than "read". The convenience of a single page hides that difference.
Self-check
- On your aggregate page, can you tell which numbers came from asking the service directly and which came from scraping a file?
- If a scraped source changes format, does it surface as "parse failed" rather than as "0" or "none"?
- If you use file mtime as freshness, does an empty or partially written file from a failed job also look "fresh"?
The honest part
The only solid fact in this post is one commit. A 159-line hub exists, two sources are read as JSON, two via mtime and regex, and it only runs inside the tailnet. Whether the regex has actually broken since, or whether this hub really changed my habit of opening four tabs, I have no record of, so I cannot say. I also know the right next step is JSON endpoints for the two static sources. I have not decided when to do that.