You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Executive dashboard lead_source filter offers advertising, a value no record can hold — the canonical picklist value is advertisement #1849
Found while translating dashboard filter chrome for #1822. Not fixed there — that card's surface is the labels, this is the option value, a different defect.
The defect
src/dashboards/executive.dashboard.ts declares the lead_source global filter with six static options:
advertising is not a value the field can hold. The canonical set is LEAD_SOURCE_OPTIONS in src/objects/_picklists.ts, shared by crm_lead, crm_contact and crm_opportunity (#490), and its advertising member is spelled advertisement:
So picking Advertising on the Executive Overview dashboard ANDs lead_source = 'advertising' into every bound widget's query, which matches no row: the whole dashboard goes to zero. Nothing errors — a dashboard filter is matched against the object behind each widget's dataset, and an unmatched value is simply an empty result set, so this reads as "no pipeline from advertising" rather than as a broken control.
A second, milder half
The same filter enumerates only 6 of the canonical 12 values. webinar, paid_search, social, content, email_campaign and other cannot be selected at all, so a reader cannot slice the executive view by them even though records carry them. Whether that is deliberate curation or drift is a judgement call for triage — the advertising misspelling is not.
Suggested shape
Either spell the value advertisement, or drop the static options and let the filter source them from the field's own picklist so the two cannot drift again. If the six-value curation is deliberate, a comment beside the code should say so (AGENTS.md metadata rule 11).
Not verified in a browser
Read off the source and cross-checked against src/objects/_picklists.ts; I did not drive the dashboard with that option selected. The claim that the filter narrows to zero rows follows from the documented filter semantics (.changeset/dashboard-global-filters-resolve-on-the-object.md), not from a measurement.
Dispatched by the hotcrm PM seat (#1353). The dev inherits this claim — ⛔ no second Claim:, ⛔ do not touch the assignee.
⚠️Base off main AFTER PR #1851 lands (armed for the queue). It rewrites this exact filter's option labels into inline locale maps; basing before it means a guaranteed conflict on the lines you are changing. Model: Opus. Deliverable: one PR, ready for review, pnpm verify green. ⛔ Do not arm auto-merge, ⛔ do not merge.
PM verification — the silent-zero half is confirmed, exactly
grep -rn "advertising" src/ returns exactly one hit — src/dashboards/executive.dashboard.ts:67. It is not a second spelling in use anywhere; it is a typo with no referent.
Four objects consume the canonical constant (lead · opportunity · contact · mappings/_shared), so no record can ever hold advertising.
⇒ Selecting Advertising ANDs an unmatchable value into every bound widget and the whole dashboard goes to zero, silently — no error, no empty-state explanation, just a dashboard that looks like a business with no pipeline. That is the worst failure shape a filter has.
What to fix
The value: advertising → advertisement. Mandatory.
The roster: 6 of 12 → all 12. A hand-copied subset of a canonical picklist is the same drift class one layer over, and it silently denies the user five real sources plus Other.
⭐ Prefer deriving from LEAD_SOURCE_OPTIONS over hand-copying it again — if the option list can be built from the constant (values from it, the locale map keyed by value), the roster cannot drift a second time. If that cannot be expressed cleanly inside GlobalFilterSchema's option shape, hand-copy and say in a comment why, naming the constant so the next reader can diff them.
src/dashboards/executive.dashboard.ts, plus src/translations/** only if a pack genuinely lacks a lead-source label you need.
⚠️ Check whether any other dashboard filter hand-copies a picklist subset — the service dashboard's priority and the sales dashboard's type are the obvious neighbours. If one is also short or misspelled, that is the same defect and belongs in this PR; if they are clean, say so with the command that shows it.
⭐ Green CI cannot show this. Drive it in the browser: open the Executive dashboard under zh-CN (or en), select the fixed Advertisement option, and show a bound widget returning non-zero where the old value returned zero. Report the before/after numbers. pnpm verify stage by stage with every exit code captured before any pipe, plus test-file / test counts.
⚠️test/dashboard-agent-global-filter.test.ts and test/docs-quick-tour-navigation.test.ts read filter labels and were made shape-tolerant by PR #1851 — if adding options moves either, report it; ⛔ do not add or change an assertion.
{
"issue": 1849,
"status": "done",
"branch": "claude/issue-1849-lead-source-filter-value",
"pr": "https://github.com/objectstack-ai/hotcrm/pull/1856",
"premise_still_valid": true,
"summary": "Both halves fixed in src/dashboards/executive.dashboard.ts, the only file changed besides the changeset. (1) The value: `advertising` is gone. (2) The roster: the hand-copied six are replaced by a derivation over LEAD_SOURCE_OPTIONS — `options: LEAD_SOURCE_OPTIONS.map(({ value }) => ({ value, label: LEAD_SOURCE_FILTER_LABELS[String(value)] }))` — so the control offers all twelve values a record can hold and a source added to the canonical set reaches this filter by existing. Only the wording is still authored, keyed by stored value, because GlobalFilterSchema.object is inert in this Console build (#1822); every string is the language pack's own, so `advertisement` renders Advertisement and `event` renders Event / Trade Show rather than the coined Event. No src/translations/** edit was needed — all twelve labels already exist in all four packs. PM verification held on re-check: 12 canonical values, one `advertising` hit, four consumers of the constant. Neighbours checked and CLEAN, mechanically, against the built artifact: `sales_dashboard.type vs crm_opportunity.type: offered=4 canonical=4 missing=[] PHANTOM=[]` and `service_dashboard.priority vs crm_case.priority: offered=4 canonical=4 missing=[] PHANTOM=[]`; the same script over the base artifact is non-vacuous — it prints `executive_dashboard.lead_source ... offered=6 canonical=12 missing=[webinar,advertisement,paid_search,social,content,email_campaign,other] PHANTOM=[advertising]` while clearing both neighbours in the same pass. The other two globalFilters resolve through optionsFrom and have no static roster. Nothing under test/** touched, no screenshot re-captured, no assignee written, no auto-merge.",
"tests": "pnpm verify stage by stage, each exit captured BEFORE any pipe: validate=0, typecheck=0, lint=0 (4 pre-existing unrelated warnings), lint:i18n-gate=0 ('i18n lint gate: 0 i18n/missing-* issues'), hygiene=0 ('source hygiene clean'), hygiene:tokens=0 (interaction layer 38,335 -> 38,585 of 40,000, headroom 1,415; authored total 138,265 -> 138,515 of 140,000), build=0, test=0. Test files/tests IDENTICAL before and after: 165 passed (165), 3455 passed | 1 skipped (3456) on the base worktree and on the fix. Neither shape-tolerant pin moved — dashboard-agent-global-filter reads the SERVICE dashboard's control, docs-quick-tour-navigation collects filter-level labels only, never option labels. BROWSER PROOF (Chromium at /opt/pw-browsers, dev server booted, ONE database seeded once and never reset; the before half is the base metadata c423673 rebuilt and served, not a reconstruction): Open Leads reads 21 unfiltered, 0 under the old Advertising, 2 under the fixed Advertisement. Dropdown 7 entries -> 13. HONEST CAVEAT, measured not expected: the opportunity- and contact-bound tiles read 0 under BOTH values — the seed carries exactly one `advertisement` opportunity (Stark Expansion (Lost), closed_lost) and every exec opportunity widget filters to closed_won or open pipeline, and no contact carries `advertisement` — so the fix is observable on the leads tile and nowhere else on this seed data; Active Accounts holds at 9 because it declares filterBindings lead_source:false. Re-issuing the captured lead_metrics payload against the same running app with only the value swapped: lead_count 2 for advertisement, 0 for advertising, identical SQL. Under zh-CN all 13 render in the pack's wording. ABLATION (from the committed tree, mutation proved on disk first, grep -c webinar 1 -> 0): deleting one label row makes `pnpm validate` exit 1 with 'dashboards.2.globalFilters.1.options.3.label: Invalid input' — the roster cannot drift back silently. Restored by `git checkout HEAD -- src/dashboards/executive.dashboard.ts`, proved by blob hash d7f91539298af3ab3690b3a3f7404234656a258b on both sides with `git diff HEAD` empty; artifact rebuilt to 12 options afterwards.",
"mcp_calls": "5 — issue_read(get_comments #1849), pull_request_read(#1851), create_pull_request(#1856), pull_request_read(#1856 body read-back), add_issue_comment(this report)",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
os-steve commentedon Sep 10, 2026
Claim:session_01DuzfS5chho38Yx1jxx9DEj→ branchclaude/issue-1849-lead-source-filter-valueDispatched by the hotcrm PM seat (#1353). The dev inherits this claim — ⛔ no second
Claim:, ⛔ do not touch the assignee.mainAFTER PR #1851 lands (armed for the queue). It rewrites this exact filter's option labels into inline locale maps; basing before it means a guaranteed conflict on the lines you are changing.Model: Opus. Deliverable: one PR, ready for review,
pnpm verifygreen. ⛔ Do not arm auto-merge, ⛔ do not merge.PM verification — the silent-zero half is confirmed, exactly
LEAD_SOURCE_OPTIONS(src/objects/_picklists.ts:63-76) ships 12 values:web·referral·event·webinar·partner·advertisement·paid_search·social·content·cold_call·email_campaign·other.grep -rn "advertising" src/returns exactly one hit —src/dashboards/executive.dashboard.ts:67. It is not a second spelling in use anywhere; it is a typo with no referent.lead·opportunity·contact·mappings/_shared), so no record can ever holdadvertising.⇒ Selecting Advertising ANDs an unmatchable value into every bound widget and the whole dashboard goes to zero, silently — no error, no empty-state explanation, just a dashboard that looks like a business with no pipeline. That is the worst failure shape a filter has.
What to fix
advertising→advertisement. Mandatory.⭐ Prefer deriving from
LEAD_SOURCE_OPTIONSover hand-copying it again — if the option list can be built from the constant (values from it, the locale map keyed by value), the roster cannot drift a second time. If that cannot be expressed cleanly insideGlobalFilterSchema's option shape, hand-copy and say in a comment why, naming the constant so the next reader can diff them.advertisement's canonical English label is Advertisement, not Advertising.Scope
src/dashboards/executive.dashboard.ts, plussrc/translations/**only if a pack genuinely lacks a lead-source label you need.priorityand the sales dashboard'stypeare the obvious neighbours. If one is also short or misspelled, that is the same defect and belongs in this PR; if they are clean, say so with the command that shows it.test/**(epic Epic: bring this repo's test farm back under the 2026-08-31 ruling — platform-first,os lint --strictfirst, then retire the local re-implementations by family #1579's fence). ⛔ Nocontent/docs/releases/edit. ⛔ Do not re-captureassets/screenshots/**.Proof obligation
⭐ Green CI cannot show this. Drive it in the browser: open the Executive dashboard under
zh-CN(oren), select the fixed Advertisement option, and show a bound widget returning non-zero where the old value returned zero. Report the before/after numbers.pnpm verifystage by stage with every exit code captured before any pipe, plus test-file / test counts.test/dashboard-agent-global-filter.test.tsandtest/docs-quick-tour-navigation.test.tsread filter labels and were made shape-tolerant by PR #1851 — if adding options moves either, report it; ⛔ do not add or change an assertion.Generated by Claude Code
os-steve commentedon Sep 10, 2026
os-dev-report
{ "issue": 1849, "status": "done", "branch": "claude/issue-1849-lead-source-filter-value", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1856", "premise_still_valid": true, "summary": "Both halves fixed in src/dashboards/executive.dashboard.ts, the only file changed besides the changeset. (1) The value: `advertising` is gone. (2) The roster: the hand-copied six are replaced by a derivation over LEAD_SOURCE_OPTIONS — `options: LEAD_SOURCE_OPTIONS.map(({ value }) => ({ value, label: LEAD_SOURCE_FILTER_LABELS[String(value)] }))` — so the control offers all twelve values a record can hold and a source added to the canonical set reaches this filter by existing. Only the wording is still authored, keyed by stored value, because GlobalFilterSchema.object is inert in this Console build (#1822); every string is the language pack's own, so `advertisement` renders Advertisement and `event` renders Event / Trade Show rather than the coined Event. No src/translations/** edit was needed — all twelve labels already exist in all four packs. PM verification held on re-check: 12 canonical values, one `advertising` hit, four consumers of the constant. Neighbours checked and CLEAN, mechanically, against the built artifact: `sales_dashboard.type vs crm_opportunity.type: offered=4 canonical=4 missing=[] PHANTOM=[]` and `service_dashboard.priority vs crm_case.priority: offered=4 canonical=4 missing=[] PHANTOM=[]`; the same script over the base artifact is non-vacuous — it prints `executive_dashboard.lead_source ... offered=6 canonical=12 missing=[webinar,advertisement,paid_search,social,content,email_campaign,other] PHANTOM=[advertising]` while clearing both neighbours in the same pass. The other two globalFilters resolve through optionsFrom and have no static roster. Nothing under test/** touched, no screenshot re-captured, no assignee written, no auto-merge.", "tests": "pnpm verify stage by stage, each exit captured BEFORE any pipe: validate=0, typecheck=0, lint=0 (4 pre-existing unrelated warnings), lint:i18n-gate=0 ('i18n lint gate: 0 i18n/missing-* issues'), hygiene=0 ('source hygiene clean'), hygiene:tokens=0 (interaction layer 38,335 -> 38,585 of 40,000, headroom 1,415; authored total 138,265 -> 138,515 of 140,000), build=0, test=0. Test files/tests IDENTICAL before and after: 165 passed (165), 3455 passed | 1 skipped (3456) on the base worktree and on the fix. Neither shape-tolerant pin moved — dashboard-agent-global-filter reads the SERVICE dashboard's control, docs-quick-tour-navigation collects filter-level labels only, never option labels. BROWSER PROOF (Chromium at /opt/pw-browsers, dev server booted, ONE database seeded once and never reset; the before half is the base metadata c423673 rebuilt and served, not a reconstruction): Open Leads reads 21 unfiltered, 0 under the old Advertising, 2 under the fixed Advertisement. Dropdown 7 entries -> 13. HONEST CAVEAT, measured not expected: the opportunity- and contact-bound tiles read 0 under BOTH values — the seed carries exactly one `advertisement` opportunity (Stark Expansion (Lost), closed_lost) and every exec opportunity widget filters to closed_won or open pipeline, and no contact carries `advertisement` — so the fix is observable on the leads tile and nowhere else on this seed data; Active Accounts holds at 9 because it declares filterBindings lead_source:false. Re-issuing the captured lead_metrics payload against the same running app with only the value swapped: lead_count 2 for advertisement, 0 for advertising, identical SQL. Under zh-CN all 13 render in the pack's wording. ABLATION (from the committed tree, mutation proved on disk first, grep -c webinar 1 -> 0): deleting one label row makes `pnpm validate` exit 1 with 'dashboards.2.globalFilters.1.options.3.label: Invalid input' — the roster cannot drift back silently. Restored by `git checkout HEAD -- src/dashboards/executive.dashboard.ts`, proved by blob hash d7f91539298af3ab3690b3a3f7404234656a258b on both sides with `git diff HEAD` empty; artifact rebuilt to 12 options afterwards.", "mcp_calls": "5 — issue_read(get_comments #1849), pull_request_read(#1851), create_pull_request(#1856), pull_request_read(#1856 body read-back), add_issue_comment(this report)", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code