Skip to content

Give the dashboard module panels, starting with the SMS module - #37

Merged
DorwardTech merged 2 commits into
claude/zone3-darwin-internal-tool-YQKKNfrom
claude/dashboard-module-widgets
Jul 25, 2026
Merged

Give the dashboard module panels, starting with the SMS module#37
DorwardTech merged 2 commits into
claude/zone3-darwin-internal-tool-YQKKNfrom
claude/dashboard-module-widgets

Conversation

@DorwardTech

Copy link
Copy Markdown
Owner

The mechanism first

The dashboard was one hardcoded section. A module now contributes its own panel through ProvidesDashboardWidgets — the same optional-contract shape as HasUserGuide, resolved through ModuleRegistry.

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:

September 2026 is waiting for you to approve it
12 recipients, 14 credits. Nothing is sent until you do. → Review & approve

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:

Card Why it's there
Replies, 7 days Is there anything to read — and how many read as booking interest. A window, not all time: a total that only goes up stops meaning anything.
Last sent Which month went out, and when.
Do Not Contact The permanent exclusion count.
Bookeo write-back Normally "up to date". Red when lines haven't reached a customer file.

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 same ReplyClassifier the 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 DashboardSummary grows 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's html_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

  1. The panel rendering (waiting batch, its counts, the deep link, the balance warning, oldest-first when two wait, silence when none), the reply window excluding STOPs and out-of-window messages, the backlog card both ways, the panel disappearing with the module, the audit section surviving underneath, auth — and the isolation test, where a manifest that throws leaves the dashboard rendering.

The counting assertions run against DashboardSummary rather 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.com is blocked by the proxy), so CI is the verification.


Generated by Claude Code

claude added 2 commits July 25, 2026 11:31
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
DorwardTech marked this pull request as ready for review July 25, 2026 11:41
@DorwardTech
DorwardTech merged commit 182fa88 into claude/zone3-darwin-internal-tool-YQKKN Jul 25, 2026
2 checks passed
@DorwardTech
DorwardTech deleted the claude/dashboard-module-widgets branch August 3, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants