Skip to content

Executive dashboard lead_source filter offers advertising, a value no record can hold — the canonical picklist value is advertisement #1849

Closed

Description

@os-steve

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:

{ value: 'web' }, { value: 'referral' }, { value: 'partner' },
{ value: 'event' }, { value: 'cold_call' }, { value: 'advertising' }

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:

{ label: 'Advertisement',   value: '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.

Activity

  1. added
    bugSomething isn't working
    metadataDeclarative metadata — schema, security posture, UI surfaces
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 10, 2026
  2. self-assigned this
    on Sep 10, 2026
  3. os-steve commented on Sep 10, 2026

    @os-steve
    CollaboratorAuthor

    Claim: session_01DuzfS5chho38Yx1jxx9DEj → branch claude/issue-1849-lead-source-filter-value

    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

    • 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.
    • 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

    1. The value: advertising → advertisement. Mandatory.
    2. 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.
    3. Locale maps for any option you add, in PR i18n(dashboards): translate global filter names and their option labels #1851's style and wording — from the language packs, ⛔ not coined. advertisement's canonical English label is Advertisement, not Advertising.

    Scope

    Proof obligation

    ⭐ 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.


    Generated by Claude Code

  4. os-steve commented on Sep 10, 2026

    @os-steve
    CollaboratorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions