Give the dashboard module panels, starting with the SMS module - #37
Merged
DorwardTech merged 2 commits intoJul 25, 2026
Merged
Conversation
The dashboard was one hardcoded section. A module now contributes its own panel through ProvidesDashboardWidgets, the same optional-contract shape as HasUserGuide, so the core holds no reference to any module: disable the SMS module and the panel goes with it, rather than leaving the landing page querying tables that may no longer exist. Unlike navigation(), gathering a widget runs queries, so the registry calls each module inside a try/catch — a module enabled before its migrations ran is dropped with a logged warning instead of 500ing the page every admin lands on. There is a test for that, because the failure mode is the page nobody can route around. The SMS panel leads with the one state where nothing happens until somebody clicks: a batch waiting for approval, with its recipient and credit counts, a balance warning if the account looks short, and a button straight to it. Beside that, replies in the last seven days and how many read as booking interest, the last month sent, the Do Not Contact total, and the Bookeo write-back backlog. That last one earns its place by being invisible everywhere else. The write-back is best-effort by design — it must never fail a send that already went out — so a Bookeo outage silently drops customer-file lines and no screen said so. Now one does, in red, and it clears itself as the hourly retry catches up. Booking interest is a judgement on message text, not a column, so it reads bodies rather than counting rows: capped at 200, and through the same classifier the Replies page renders with, because a dashboard saying 2 next to a list showing 3 is worse than no number. Also corrects a comment on the guide view claiming Str::markdown() escapes raw HTML. It doesn't — the controller's html_input => 'strip' is what makes that page safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
Blade emits the newline between the recipient and credit counts, so "12 recipients, 14 credits" reached the page as "12 recipients,\n 14 credits" — fine visually, since HTML collapses whitespace, but not the contiguous sentence the assertion (or a grep) looks for. Both counts now sit on one source line, with a note saying why, and the last-sent line gets the same treatment for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
DorwardTech
marked this pull request as ready for review
July 25, 2026 11:41
DorwardTech
merged commit Jul 25, 2026
182fa88
into
claude/zone3-darwin-internal-tool-YQKKN
2 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The mechanism first
The dashboard was one hardcoded section. A module now contributes its own panel through
ProvidesDashboardWidgets— the same optional-contract shape asHasUserGuide, resolved throughModuleRegistry.That indirection is the point rather than ceremony: a dashboard hardcoded to query a module's tables breaks the moment that module is disabled, and this is the page every admin lands on. Disable the SMS module and the panel goes with it.
Unlike
navigation(), gathering a widget runs queries, so the registry calls each module inside a try/catch — a module enabled before its migrations have run is dropped with a logged warning instead of 500ing the landing page. There's a test for that, because it's the one page nobody can route around.The dashboard owns the card, heading and spacing; a module supplies a view name and its data. Widgets from different modules will sit together instead of each inventing a container.
The SMS panel
It leads with the one state where nothing happens until somebody clicks — a batch waiting for approval:
In amber, with the balance warning folded in if the account looks short of credits. No amber box means nothing is waiting.
Then four numbers:
Why the Bookeo backlog earns a card
It's the only number here that reports a fault, and it's there because it is invisible everywhere else. The write-back is best-effort by design — it must never fail a send that already went out — so when the Bookeo key expired, every message and reply for a day silently failed to reach a customer file and no screen said so. Now one does, and it clears itself as the hourly retry catches up. A number that stays put means Bookeo is refusing us.
It reuses
BookeoLogBackfill::pending(), which by design never calls Bookeo.Two judgement calls
Booking interest is computed, not counted. It's a judgement on message text, not a stored column, so it can't be a SQL aggregate. Rather than drop the most useful line in the panel, it reads bodies for the reply window — capped at 200 rows, selecting only
body— and runs the sameReplyClassifierthe Replies page renders with. A dashboard saying 2 next to a list showing 3 would be worse than no number.STOPs are excluded from the reply count. They're handled automatically and need nobody's attention; counting them would pad "things to read" with things already dealt with.
Everything else is a handful of aggregates plus at most two rows — nothing in
DashboardSummarygrows with the size of the tables, because it runs on every dashboard render.Also in here
A comment on the guide view claimed
Str::markdown()escapes raw HTML. It doesn't — CommonMark's default is to pass it through, and the controller'shtml_input => 'strip'is what makes that page safe. I corrected the same claim in the controller in #34 and missed this copy of it.Tests
The counting assertions run against
DashboardSummaryrather than the HTML: a bare "1" in a page could come from anywhere, and pinning the markup around it would break on any restyle.vendor/can't be installed in this environment (codeload.github.comis blocked by the proxy), so CI is the verification.Generated by Claude Code