Task: Badge palette swatch rendering + data-driven fallback badge
Table of Contents
This page documents a task in the Badge colour scheme: visual polish and self-badging fixes story. It captures the goal, current status, acceptance, and any notes or results.
Goal
(Describe what user-visible-or-internal change this task produces.)
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Badge colour scheme: visual polish and self-badging fixes |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-15 |
Acceptance
- BadgeDefinitionMdiWindow's background_colour/text_colour columns render as colour swatches (background_colour at minimum; render the actual badge itself using its own colours where practical, not just a raw swatch, so the list doubles as the palette review the story exists for).
- BadgeDefinitionDetailDialog's colour fields use a QColorDialog-backed picker instead of a raw text field.
- BadgeSeverityMdiWindow's own colour/severity linkage to badge_definition is visible in the UI – today it's not obvious how a badge_severity relates to the badge_definitions that reference it (severity_code is a soft FK with no visible reverse link).
- BadgeDefinitionMdiWindow gains a toolbar button (matching the "Related entity shortcuts" codegen facet already used elsewhere, e.g. Book's toolbar) linking to Badge Severities and Badge Mappings, so a reviewer can navigate the whole badge catalogue from one screen instead of three separate, disconnected menu entries.
- A reserved badge_definition row (e.g. code=__unmapped__ or an is_fallback flag) replaces color_constants::badge_fallback; BadgeCache/delegates resolve it the same way as any other badge instead of hardcoding it.
- The fallback's visual treatment (e.g. dashed/hatched border, or distinct from any real grey badge in use) is visibly different from a legitimate inactive/disabled badge.
- Manual QA: temporarily remove a badge_mapping row, confirm the affected badge renders as the new fallback style, not a colour indistinguishable from 'inactive'.
Discovered during task 1's manual QA (feedback captured verbatim in
that task's scenario Notes): plain background-colour-as-cell-fill
(no pill shape) works but reads worse than a real badge; task 1 fixed
this for its own two columns via EntityItemDelegate=/=badge_centered
– this task's swatch work should use the same delegate/column-style
mechanism for consistency, not reinvent a second rendering approach.
Plan
badge_definition is already on the standard codegen stack
(regenerated in 772982ea5), but two Qt detail-dialog/list-delegate
facets it needs don't exist in codegen yet — this is not a plain
model-declaration task, it starts with two small codegen extensions.
Investigation findings:
BadgeDefinitionMdiWindowsets no item delegate at all today;BackgroundColour~/~TextColourrender as raw hex-string text (ClientBadgeDefinitionModel::data()).BadgeDefinitionDetailDialoghas no UI fields at all forbackground_colour~/~text_colour~/~severity_code~/~display_order— onlycode~/~name~/~descriptionexist. This is becauseores.dq.badge_definition.orgnever declares a** Detail fieldstable under** Qt, socore.py's auto-generate fallback (core.py:2256) only emits key/name/description.- Codegen's detail-field vocabulary
(
line_edit/text_edit/static_combo/dynamic_combo/flagged_combo/check_box/spin_box, seecore.py:2290-2345) has no colour-picker type. Needs adding before the model can declare one. - Codegen's list-column badge facet (
is_badge,cpp_qt_mdi_window.cpp.mustache:245) always resolves viaBadgeCache::resolve(badge_key, value)— i.e. "this column's value is a code that points at a separate badge_definition".background_colour~/~text_colourare the opposite: they are the colour, not a code to look up. Needs a second resolver shape, not reuse ofis_badgeas-is. BadgeSeverityMdiWindow~/~ClientBadgeSeverityModelhave no reverse-FK facet in codegen at all — the "which badge_definitions reference this severity" column has to be hand-added.- Fallback:
ColorConstants::badge_fallback(orange/white) is the hardcoded pairEntityItemDelegate::paint()falls back to. Real seed rows live indq_badge_system_populate.sqlviaores_dq_badge_definitions_upsert_fn.
Steps:
- Codegen: add a
colourdetail-field type — flag computation incore.py, swatch-button +QColorDialogblock inqt_detail_dialog_ui.mustache, wiring in the detail-dialog impl mustache. - Codegen: add a self-colour list-column style (parallel to
is_badge) whose generated resolver reads the row's own two colour fields instead of callingBadgeCache::resolve. - Model: add
** Detail fieldstoores.dq.badge_definition.org(code/name/description as today;background_colour~/~text_colourascolour;severity_codeasdynamic_combofetchingbadge_severity;display_orderasspin_box); flagBackgroundColour~/~TextColourin "Columns (Qt model)" with the new self-colour style; add** Related entity shortcuts(Severities, Mappings). - Regenerate; reconcile hand-wiring at the controller/plugin level
(
BadgeDefinitionController→DqPlugin, mirroringBookController~/~RefdataPlugin'sshowBookStatusesRequestedpattern). - Hand-add the
BadgeSeverityMdiWindowreverse-link column/widget — no codegen facet for this exists. - Fallback: reserved
badge_definitionrow (code='__unmapped__') indq_badge_system_populate.sql;BadgeCache::fallback()accessor; switch both the codegen-generatedis_badgeresolver template andEntityItemDelegate's built-in fallback offColorConstants::badge_fallbackonto it; distinct dashed-border treatment inDelegatePaintUtilsso it doesn't read as inactive/grey. - Manual QA scenario per acceptance (remove a
badge_mappingrow, confirm fallback rendering).
Notes
Manual QA against the fallback badge (removing a badge_mapping row
via SQL) surfaced that BadgeCache never subscribed to any
change-notification event – only loaded once at login. Fixed by
mirroring ChangeReasonCache's subscribe-and-reload pattern (see
How does a Qt client cache stay fresh?); badge_definition edits
(e.g. a colour change) now propagate live to every client.
badge_mapping itself still has no live propagation – it is a
junction row with no generated entity handler, so it publishes no
NATS change-event at all (confirmed true of every junction in the
codebase, not just this one). Removing/restoring a mapping requires a
client reconnect to be picked up. Closing this properly is tracked in
Junction eventing: extend codegen with notify-trigger, changed-event, and event-registrar facets for the junction model type,
filed as its own sprint 24 story rather than folded into this task.
Also discovered while manually testing the fallback: badge data
(badge_severity=/=badge_definition=/=code_domain=/=badge_mapping)
is duplicated per-tenant – the system-tenant seed rows created by
dq_badge_system_populate.sql get copied into each tenant's own
tenant_id by the "provision tenant" bundle-publish step, and
badge_service reads scoped to the caller's tenant. Any manual
SQL edit/restore against badge data must target the tenant under
test (e.g. via ores_iam_tenant_by_code_fn('barclays')), not the
system tenant – an easy mistake to make since the seed script itself
only ever writes system-tenant rows.
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
| Badge Definition: swatch rendering, colour picker, and related-entity shortcuts | PASSED | Eventing not exercised by this scenario – see the fallback/eventing scenario below |
| Verify data-driven fallback badge and badge eventing | PASSED | Covers step 6 (fallback badge) and the eventing gap noted above; badge_mapping's per-tenant-duplicate + no-live-event limitation confirmed and tracked separately |
PRs
| PR | Title |
|---|---|
| #1704 | [qt,codegen] Badge palette swatch rendering and data-driven fallback badge |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | No guard against deleting the reserved __unmapped__ badge via the UI |
BadgeDefinitionDetailDialog.cpp |
Accepted | Added a delete-time guard; rename is already prevented since code is read-only outside create mode |
| 2 | BadgeCache::onNotificationReceived can stack multiple one-shot refreshed() connections while a reload is in flight |
BadgeCache.cpp |
Accepted | Only add the one-shot connection when a reload isn't already in flight |
| 3 | validateInput() doesn't explicitly validate colour fields |
BadgeDefinitionDetailDialog.cpp |
Declined | By construction: ColourSwatchHelper normalises any invalid hex to #ffffff, so there's no invalid state to catch |
Result
All acceptance criteria met:
BadgeDefinitionMdiWindow's Background/Text columns render as real coloured badges viaEntityItemDelegate=/=badge_centered, the same mechanism task 1 established – not a second rendering approach.BadgeDefinitionDetailDialoggained a full Detail fields set (colour-picker swatches for background/text, severity dynamic_combo, display_order spin_box) via a newcolourcodegen detail-field type and a new self-colour list-column style – codegen extensions, not a one-off hand-wire.BadgeSeverityMdiWindowshows its reverse-FK link to referencing badge_definitions (hand-added; no codegen facet exists for this yet).BadgeDefinitionMdiWindowgained the "Related entity shortcuts" toolbar (Severities, Mappings), matching the pattern used elsewhere (e.g. Book).- Fallback: reserved
badge_definitionrow (code'unmapped') replaces =ColorConstants::badge_fallbackat every badge-rendering call site;BadgeCache::fallback()resolves it the same wayresolve()resolves any other badge. Dashed-border treatment inDelegatePaintUtilsmakes it visibly distinct from any real grey/inactive badge. BadgeCachenow subscribes tobadge_definition_changedand reloads on receipt (mirroringChangeReasonCache), so a colour edit propagates live to every running client, not just the definitions list window that made the edit.- Manual QA scenario executed end-to-end (removed the
party_status=/ =Activemapping via SQL, confirmed the fallback badge rendered with the correct dashed/orange treatment, confirmed abadge_definitioncolour edit propagates live between two client instances, restored seed data) – both test scenarios PASSED.
Two gaps surfaced during manual QA and deliberately scoped out rather than folded into this task, each filed as its own backlog item (see
Notes above for detail): junctions (including badge_mapping) have
no live NATS eventing at all in codegen today (Junction eventing story); and a general FK-like/lookup-entity badge-vs-image-vs-plain-text rendering policy doesn't exist yet (FK-like/lookup entity rendering policy story).