Skip to content

Promote stage → main (prod deploy) - #374

Merged
ani-shh merged 3 commits into
mainfrom
stage
Aug 10, 2026
Merged

Promote stage → main (prod deploy)#374
ani-shh merged 3 commits into
mainfrom
stage

Conversation

@ani-shh

@ani-shh ani-shh commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

No migrations in this promote.

Test plan

  • All 3 verified on stage
  • CI green on each

Summary by CodeRabbit

  • New Features

    • Added an advanced filter bar with searchable fields, operators, values, filter chips, and AND/OR controls.
    • Added URL-backed filters that persist across navigation and support dates, ranges, lists, and multiple criteria.
    • Added accurate source and assignee facet counts, including combined facet requests.
    • Search now includes lead display IDs.
  • Bug Fixes

    • Prevented invalid or unsupported filters from becoming unintended broad matches.
    • Improved browser compatibility for saved filter state.
    • Preserved the legacy toolbar as a fallback when advanced filters are disabled.

sthasadin and others added 3 commits August 7, 2026 11:18
…um (#371)

Phase 3 (ADVANCED-FILTERS-BRIEF): Notion/Twenty-style filter bar
(AdvancedFilterBar) wired into leads-table.tsx behind
NEXT_PUBLIC_ADVANCED_FILTERS, URL-backed via ?f= (use-advanced-filters.ts),
reusing FilterOptionList verbatim for multi-select. compileAssignees now
drops no-op conditions instead of emitting a tautology inside OR groups.

Phase 3 addendum — facet counts (regression fix + server-side move):
- A/B: restored counts + zero-count hiding on Assigned To and Collaborators
  in advancedFilterOptionOverrides (parity with the legacy filterDefs).
- C: server-side Assigned-To facet via lead_aggregates()'s existing
  `counselor` dimension (getAssigneeFacet, aggregates.ts) — exact,
  tenant-wide counts instead of the 25-row-page client computation.
  ?facets= now accepts a comma-separated dimension list
  (?facets=source,assignee) in one client round-trip; ?facets=source alone
  stays byte-identical (KanbanBoard.tsx depends on it). Collaborator counts
  stay client-side/page-scoped, labelled as such — no lead_aggregates()
  dimension exists for that join table yet.
- D: no industry gate — assignee counts gate on isAdmin/isTeamScoped only.
- E: tree-to-aggregate-params.ts pulled forward from Phase 5 — a pure-AND,
  fully-expressible ?f= tree now drives real facet counts instead of a
  blanket counts:null; anything inexpressible (OR groups, contains,
  unknown fields/ops) still falls back to counts:null, pino-logged with
  the reason.
- G: Apply bug root-caused via browser repro (Sadin) — two fixes:
  1. serialize.ts used Buffer.from(...).toString("base64url"), which
     throws "Unknown encoding: base64url" in a real browser bundle (the
     `buffer` shim doesn't support it, even though Node's real Buffer
     does) — handleApply threw before setOpen(false), so the popover
     never closed and the URL never gained ?f=. Replaced with an
     isomorphic TextEncoder/TextDecoder + btoa/atob codec. Added
     serialize.browser.test.ts (jsdom + Buffer deleted) so a Node-only
     API can't pass CI silently again — plain jsdom alone doesn't catch
     this, since jsdom still runs on Node's real Buffer.
  2. filter-value-editor.tsx: selecting a multi-select option reveals
     FilterOptionList's "Clear" row, pushing Apply down ~45px under the
     cursor. Reserved that row's height with a spacer whenever Clear
     isn't shown, so the container's total height never changes.

Verified live on local dev (browser): Assigned To shows exact tenant-wide
counts (Unassigned 27 + counselor 6 = 33, zero-count members absent);
Apply button position stable across the Clear-row transition; Apply
correctly gains ?f=, closes the popover, and filters the table; two
stacked chips (Assigned To + Status) compose correctly; a copied ?f= URL
reproduces the same filtered view in a fresh tab.

npm run test: 1326/1326 green. npm run build clean. npx eslint
--max-warnings 50: 0 errors, 46 pre-existing warnings.

Co-authored-by: Anish Balami <anishbalami38@gmail.com>
…ge build (#372)

NEXT_PUBLIC_* is inlined at build time, so the stage image never actually
picked up the flag despite Phases 0-3 being merged (d3841a6) — the deployed
bundle rendered the legacy toolbar.

- Dockerfile: ARG + ENV NEXT_PUBLIC_ADVANCED_FILTERS alongside the existing
  NEXT_PUBLIC_* pairs.
- deploy-staging.yml: NEXT_PUBLIC_ADVANCED_FILTERS=1 literal in build-args,
  matching how NEXT_PUBLIC_SENTRY_ENVIRONMENT=staging is done.
- deploy.yml (prod) untouched — prod stays off; with no ARG value passed the
  Dockerfile ARG resolves empty and the flag is undefined there.

Also commits docs/ADVANCED-FILTERS-BRIEF.md Phase 3.5 addendum (was already
in the working tree at session start).
Search box matched name/email/phone only, so searching by lead ID
(e.g. ADM-6038) silently returned 0 results — client hit this in
prod. Add display_id to both the server field registry (leads.ts)
and the client-side legacy fallback matcher (leads-table.tsx) so
ID search works the same way global search already does.

Co-authored-by: Anish Balami <anishbalami38@gmail.com>
@ani-shh
ani-shh requested a review from sthasadin as a code owner August 10, 2026 14:53
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a feature-flagged advanced filter bar for leads. It adds URL-backed filter trees, browser-safe serialization, aggregate facet support for source and assignee, compiler no-op handling, leads-table integration, reusable UI primitives, tests, and staging-only deployment configuration.

Changes

Advanced leads filtering

Layer / File(s) Summary
Filter engine and browser serialization
src/lib/filters/*, package.json
Filter compilation can omit null predicates. Lead search includes display_id. Filter-tree serialization works without Node Buffer.
Aggregate translation and facet API
src/lib/filters/tree-to-aggregate-params.ts, src/lib/leads/aggregates.ts, src/app/(main)/api/v1/leads/*
Expressible filter trees map to aggregate parameters. The leads API supports source and assignee facets and preserves the legacy source-only response shape.
Advanced filter editor
src/components/filters/*, src/components/ui/combobox.tsx, src/components/ui/scroll-area.tsx
New components edit fields, operators, values, conjunctions, and filter chips. URL state handles malformed, stale, empty, and oversized filter trees.
Leads-table integration and staging wiring
src/components/dashboard/leads-table.tsx, Dockerfile, .github/workflows/deploy-staging.yml, .env.example, docs/ADVANCED-FILTERS-BRIEF.md
The leads table switches between advanced and legacy filtering. Server facet counts are preferred when available. Staging enables the feature through the Docker build argument.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LeadUser
  participant LeadsTable
  participant AdvancedFilterBar
  participant LeadsRoute
  participant LeadAggregates
  LeadUser->>AdvancedFilterBar: edit and apply filter tree
  AdvancedFilterBar->>LeadsTable: update controlled filter state
  LeadsTable->>LeadsRoute: request encoded filter tree and facets
  LeadsRoute->>LeadAggregates: query filtered source and assignee aggregates
  LeadAggregates-->>LeadsRoute: return facet counts
  LeadsRoute-->>LeadsTable: return filtered leads and facets
  LeadsTable-->>LeadUser: render filtered results and chips
Loading

Possibly related PRs

Suggested reviewers: sthasadin

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a summary and test plan but omits the required base branch, freshness, gate, tenant-isolation, and reviewer-notes sections. Add the required template sections and record the branch, rebase, build, lint, tenant-isolation, and reviewer-notes checks; state why database sections do not apply.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: promoting the stage branch to main for production deployment.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stage

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.72.0)

Trivy execution failed: 2026-08-10T14:54:13Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: cloudformation scan error: fs filter error: fs filter error: walk error open .coderabbit-opengrep-fallback.8bf81df4-434d-4f7d-afdb-ae4257a2883b.yml: no such file or directory: open .coderabbit-opengrep-fallback.8bf81df4-434d-4f7d-afdb-ae4257a2883b.yml: no such file or directory


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ani-shh
ani-shh merged commit c91a857 into main Aug 10, 2026
11 of 12 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ADVANCED-FILTERS-BRIEF.md`:
- Around line 544-556: Label the component-tree fenced code block in the
documentation with the text language identifier by changing its opening fence;
leave the diagram content unchanged.

In `@src/app/`(main)/api/v1/leads/route.ts:
- Around line 511-559: Update the aggregate facet flow around baseFacetParams in
src/app/(main)/api/v1/leads/route.ts:511-559 to preserve parity with the page
query by representing legacy assigned_to, stage, and source filters in the
aggregate RPC, or return the existing counts:null fallback whenever any such
filter cannot be represented exactly. Add regression cases in
src/app/(main)/api/v1/leads/route.test.ts:817-900 covering stage, assigned_to,
and legacy source filters, asserting either the correct RPC constraints or the
no-count fallback.

In `@src/components/dashboard/leads-table.tsx`:
- Around line 1928-1970: The advanced filter registry currently exposes the
destinations field without selectable options. Update the
advancedFilterOptionOverrides or the corresponding useFilterOptions
configuration to supply destinations values through an existing override or
async loader, and ensure advancedVisibleFields does not include destinations
while those values are unavailable.
- Around line 711-738: Update the fetch chain in the facet-loading flow to check
res.ok immediately after fetch and throw for non-2xx responses before calling
res.json(). Keep the existing facet mapping in the body handler and let the
catch block preserve prior facet state for HTTP and network failures.
- Around line 366-372: Sanitize the advanced filter tree against the visible
field registry before determining activity, fetching leads, or rendering filter
controls. Update the useAdvancedFilters flow and the related leads fetch/render
logic to use the same advancedVisibleFields-filtered registry passed to the bar,
removing role- and industry-restricted conditions from decoded URL state so
hidden filters cannot affect requests or remain active without a visible chip.
- Around line 371-372: Restrict advanced filter activation in LeadsTable to
supported server-paginated data paths by incorporating serverPaginated into
advancedFiltersEnabled or the equivalent AdvancedFilterBar/render gating. Ensure
non-server-paginated consumers do not display active advanced filtering controls
while preserving existing behavior for server-paginated tables and the
fetch/filter flows around advancedFilters.tree.
- Around line 510-525: Update the advanced-filter facet aggregation flow around
treeToAggregateParams to remove the requested facet dimension, including source
and assignees, from the encoded filter tree before computing counts. Establish
one shared client/server contract for stripping the requested dimension, while
preserving all other advanced conditions and existing legacy-filter behavior.
- Around line 386-390: Remove synchronous setters from the effects in
LeadsTable: at src/components/dashboard/leads-table.tsx lines 386-390, derive
the SSR-prop values for localLeads and total during render while preserving
advancedFilterActive behavior; at lines 704-708, derive empty facet values when
facetFetchParams is null instead of clearing facet state in the effect. Update
the relevant reads to use these derived values and retain effect-driven updates
only where asynchronous side effects are required.

In `@src/components/filters/condition-defaults.ts`:
- Around line 79-84: Replace RELATIVE_DATE_PRESETS with direction-specific
preset maps for within_last and within_next, using the documented values 7d,
30d, 3m, and 1y from tree-to-aggregate-params.ts. Update filter-value-editor.tsx
to pass the appropriate map per operator and update chip-label.ts to use
direction-correct labels, avoiding undocumented 1d and 1m presets.

In `@src/components/filters/filter-value-editor.tsx`:
- Around line 114-129: Update the numeric onChange handlers in the between-range
and field.type === "number" branches of the filter value editor so clearing an
input preserves an empty draft value instead of converting it to 0. Pass the raw
input value or the component’s established empty-value representation through
onChange, while retaining numeric values for non-empty input and existing range
behavior.

In `@src/lib/filters/serialize.browser.test.ts`:
- Around line 63-72: Update the test case around encodeFilterTree so the encoded
input is produced directly with originalBuffer using the legacy server-side
Buffer base64url encoding, rather than calling encodeFilterTree. Keep the
subsequent deletion of globalThis.Buffer and decodeFilterTree assertion
unchanged.

In `@src/lib/filters/tree-to-aggregate-params.ts`:
- Around line 30-34: Update the date subtraction helper containing the `unit`
branch so month (`"m"`) and year (`"y"`) subtraction clamps to the target
calendar month’s last valid day instead of allowing `setMonth()` or
`setFullYear()` rollover; preserve day subtraction and time components, and
cover leap-day and month-end cases in tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 96c5f8a4-2e91-4ac7-ac50-33f84bb4fac3

📥 Commits

Reviewing files that changed from the base of the PR and between 8828d2e and b45e27d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • .env.example
  • .github/workflows/deploy-staging.yml
  • Dockerfile
  • docs/ADVANCED-FILTERS-BRIEF.md
  • package.json
  • src/app/(main)/api/v1/leads/route.test.ts
  • src/app/(main)/api/v1/leads/route.ts
  • src/components/dashboard/leads-table.tsx
  • src/components/filters/add-filter-button.tsx
  • src/components/filters/advanced-filter-bar.tsx
  • src/components/filters/chip-label.ts
  • src/components/filters/condition-defaults.ts
  • src/components/filters/conjunction-toggle.tsx
  • src/components/filters/filter-chip-row.tsx
  • src/components/filters/filter-chip.tsx
  • src/components/filters/filter-condition-editor.tsx
  • src/components/filters/filter-field-picker.tsx
  • src/components/filters/filter-operator-picker.tsx
  • src/components/filters/filter-value-editor.tsx
  • src/components/filters/types.ts
  • src/components/filters/use-filter-options.ts
  • src/components/ui/combobox.tsx
  • src/components/ui/filter-menu.tsx
  • src/components/ui/scroll-area.tsx
  • src/lib/filters/compile.test.ts
  • src/lib/filters/compile.ts
  • src/lib/filters/registry/leads.test.ts
  • src/lib/filters/registry/leads.ts
  • src/lib/filters/serialize.browser.test.ts
  • src/lib/filters/serialize.ts
  • src/lib/filters/tree-to-aggregate-params.test.ts
  • src/lib/filters/tree-to-aggregate-params.ts
  • src/lib/filters/types.ts
  • src/lib/filters/use-advanced-filters.ts
  • src/lib/leads/aggregates.ts

Comment on lines +544 to +556
```
AdvancedFilterBar advanced-filter-bar.tsx
├── FilterChipRow → FilterChip click a chip to edit it in place
├── ConjunctionToggle "Where / and / or" — hand-rolled 2-state, no toggle-group primitive
└── AddFilterButton "+ Add filter"
└── FilterFieldPicker Command + CommandInput + grouped items (reference screenshot 1)
└── FilterConditionEditor
├── FilterOperatorPicker options from isOperatorAllowed() (reference screenshot 2)
└── FilterValueEditor dispatch on field.type + operator arity
├── text · number (1 or 2 inputs for `between`) · date
├── select · boolean
└── multi-select ← WRAPS the existing FilterOptionList
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Set a language for the component-tree code fence.

Line 544 opens an unlabeled fenced block. Markdownlint reports MD040. Use text for this component-tree diagram.

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 544-544: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ADVANCED-FILTERS-BRIEF.md` around lines 544 - 556, Label the
component-tree fenced code block in the documentation with the text language
identifier by changing its opening fence; leave the diagram content unchanged.

Source: Linters/SAST tools

Comment on lines +511 to +559
// Effective per-axis filter values: from the ?f= tree (via treeToAggregateParams)
// when ?f= is present, else from the legacy toolbar params — exactly like the page
// query's own filterTree/legacyLeadsParamsToTree split above. `search` and the
// industry "__none__" sentinel have no tree-translation path (search is always
// `contains`, rejected above; is_empty is rejected above too) — both are legacy-only.
const effectiveStatus = rawFilterParam !== null ? (aggParams?.status ?? null) : status || null;
const assigneesIdsLegacy = assigneesTokens.filter((t) => t !== "unassigned" && UUID_RE.test(t));
const wantsUnassignedLegacy = assigneesTokens.includes("unassigned");
const effectiveAssigneesAny = rawFilterParam !== null ? (aggParams?.assigneesAny ?? null) : (assigneesIdsLegacy.length > 0 ? assigneesIdsLegacy : null);
const effectiveIncludeUnassigned = rawFilterParam !== null ? !!aggParams?.includeUnassigned : wantsUnassignedLegacy;
const validCollaboratorIdsLegacy = collaboratorIds.filter((id) => UUID_RE.test(id));
const effectiveCollaboratorIds = rawFilterParam !== null ? (aggParams?.collaboratorIds ?? null) : (validCollaboratorIdsLegacy.length > 0 ? validCollaboratorIdsLegacy : null);
const effectiveTag = rawFilterParam !== null ? (aggParams?.tag ?? null) : (tagFilter && tagFilter !== "all" ? tagFilter : null);
const effectiveProspectIndustry = rawFilterParam !== null ? (aggParams?.prospectIndustry ?? null) : (industryFilter && industryFilter !== "all" && industryFilter !== "__none__" ? industryFilter : null);
const effectiveProspectIndustryNone = rawFilterParam !== null ? false : industryFilter === "__none__";
const effectiveFormConfigId = rawFilterParam !== null ? (aggParams?.formConfigId ?? null) : (formFilter && formFilter !== "all" && UUID_RE.test(formFilter) ? formFilter : null);
const effectiveCreatedAfter = rawFilterParam !== null ? (aggParams?.createdAfter ?? null) : createdAfter;
const effectiveSearch = rawFilterParam !== null ? null : (search ? search.replace(/[,().]/g, "") : null);

const baseFacetParams = {
tenantId: auth.tenantId,
scope: facetScope,
user: facetScope === "own" ? scope.userId : null,
userBranchId: scope.userBranchId,
crossPoolSlug: scope.crossBranchPoolListSlug,
branchId: facetScope === "branch" ? scope.branchId : null,
sharedPoolAssignedToAny,
pipelineIds: auth.permissions.pipelineAccess !== "all" ? [...auth.permissions.pipelineAccess.ids] : null,
status: effectiveStatus,
collaboratorIds: effectiveCollaboratorIds,
tag: effectiveTag,
prospectIndustry: effectiveProspectIndustry,
prospectIndustryNone: effectiveProspectIndustryNone,
formConfigId: effectiveFormConfigId,
createdAfter: effectiveCreatedAfter,
// Mirror the page query's either/or (route.ts:309-316) exactly: an explicit list
// wins outright, a funnel's list set wins next, and the archive/staging exclusion
// only applies when neither is present. Passing more than one of these unconditionally
// ANDs them in lead_aggregates — for a staging list that becomes `list_id = X AND
// list_id NOT IN (…X…)`, an unsatisfiable predicate that zeroed the facet.
listIdEq: resolvedListId,
listIdAny: !resolvedListId && funnelListIds.length > 0 ? funnelListIds : null,
excludeListIds:
!resolvedListId && funnelListIds.length === 0 && excludeListIds.length > 0
? excludeListIds
: null,
search: effectiveSearch,
includeConverted,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve page-filter parity before returning legacy facet counts.

The page query applies assigned_to, stage, and legacy source filters. The aggregate parameters do not represent these filters. In particular, Lines 444-448 confirm that stage cannot reach lead_aggregates(). Facet options can therefore describe a different lead set than the table.

  • src/app/(main)/api/v1/leads/route.ts#L511-L559: Add equivalent aggregate support, or return the existing counts: null fallback when any legacy filter cannot be represented exactly.
  • src/app/(main)/api/v1/leads/route.test.ts#L817-L900: Add regression cases for stage, assigned_to, and legacy source filters. Assert that each case either constrains the RPC correctly or returns the no-count fallback.
📍 Affects 2 files
  • src/app/(main)/api/v1/leads/route.ts#L511-L559 (this comment)
  • src/app/(main)/api/v1/leads/route.test.ts#L817-L900
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(main)/api/v1/leads/route.ts around lines 511 - 559, Update the
aggregate facet flow around baseFacetParams in
src/app/(main)/api/v1/leads/route.ts:511-559 to preserve parity with the page
query by representing legacy assigned_to, stage, and source filters in the
aggregate RPC, or return the existing counts:null fallback whenever any such
filter cannot be represented exactly. Add regression cases in
src/app/(main)/api/v1/leads/route.test.ts:817-900 covering stage, assigned_to,
and legacy source filters, asserting either the correct RPC constraints or the
no-count fallback.

Comment on lines +366 to +372
const advancedFilterRegistry = useMemo(
() => leadFields({ tz: "UTC", now: new Date(0), industryId: industryId ?? null, permissions: {} } satisfies CompileCtx),
[industryId]
);
const advancedFilters = useAdvancedFilters(advancedFilterRegistry);
const advancedFiltersEnabled = process.env.NEXT_PUBLIC_ADVANCED_FILTERS === "1";
const advancedFilterActive = advancedFiltersEnabled && !isEmptyTree(advancedFilters.tree);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove hidden conditions before fetching and rendering.

useAdvancedFilters receives the full registry. advancedVisibleFields later removes role- and industry-restricted fields. A shared URL can retain a known but hidden condition, send it to /api/v1/leads, and omit its chip. The user cannot see or remove the active condition.

Decode and sanitize the tree against the same visible field registry that the bar receives.

Also applies to: 1916-1931

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/leads-table.tsx` around lines 366 - 372, Sanitize
the advanced filter tree against the visible field registry before determining
activity, fetching leads, or rendering filter controls. Update the
useAdvancedFilters flow and the related leads fetch/render logic to use the same
advancedVisibleFields-filtered registry passed to the bar, removing role- and
industry-restricted conditions from decoded URL state so hidden filters cannot
affect requests or remain active without a visible chip.

Comment on lines +371 to +372
const advancedFiltersEnabled = process.env.NEXT_PUBLIC_ADVANCED_FILTERS === "1";
const advancedFilterActive = advancedFiltersEnabled && !isEmptyTree(advancedFilters.tree);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Limit advanced mode to supported data paths.

NEXT_PUBLIC_ADVANCED_FILTERS enables AdvancedFilterBar for every LeadsTable consumer. When serverPaginated is false, the fetch effect returns at Line 552 and filtered does not apply advancedFilters.tree. Contacts and leads-organise can show the new bar while its conditions do nothing.

Gate advanced mode with serverPaginated, or add local tree evaluation for those consumers.

Also applies to: 551-563, 895-945, 2037-2050

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/leads-table.tsx` around lines 371 - 372, Restrict
advanced filter activation in LeadsTable to supported server-paginated data
paths by incorporating serverPaginated into advancedFiltersEnabled or the
equivalent AdvancedFilterBar/render gating. Ensure non-server-paginated
consumers do not display active advanced filtering controls while preserving
existing behavior for server-paginated tables and the fetch/filter flows around
advancedFilters.tree.

Comment on lines +386 to +390
useEffect(() => {
if (advancedFilterActive) return;
setLocalLeads(leads);
setTotal(initialTotal ?? leads.length);
}, [leads, initialTotal, advancedFilterActive]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant component and project lint configs/rules.
printf 'Files:\n'
git ls-files | rg '(^|/)leads-table\.tsx$|(^|/)(eslint\.config\.(js|mjs|cjs)|package\.json|\.eslintrc|tsconfig\.json)$' || true

printf '\nRelevant component outline/sections:\n'
if [ -f src/components/dashboard/leads-table.tsx ]; then
  wc -l src/components/dashboard/leads-table.tsx
  sed -n '350,410p' src/components/dashboard/leads-table.tsx
  sed -n '680,735p' src/components/dashboard/leads-table.tsx
fi

printf '\nLint config references:\n'
for f in $(git ls-files | rg '(^|/)(eslint\.config\.(js|mjs|cjs)|\.eslintrc.*|package\.json)$'); do
  echo "--- $f"
  rg -n "react-hooks|react-hooks\/set-state-in-effect|eslint-plugin|react" "$f" || true
done

Repository: Zunkireelabs/edgexcrm

Length of output: 7009


🌐 Web query:

eslint-plugin-react-hooks set-state-in-effect synchronous state updates in useEffect lint error

💡 Result:

The eslint-plugin-react-hooks rule react-hooks/set-state-in-effect is designed to prevent performance issues caused by synchronous state updates inside useEffect [1][2]. When you call setState synchronously within an effect, React is forced to perform an extra render pass because the component must re-render to apply the new state, followed by another potential effect execution [1][3]. This pattern often results in visual jank and inefficient double-rendering [1][2]. Key points regarding this lint error: Why it occurs: Synchronous updates in an effect happen after the initial render [1][2]. React must then re-render, apply DOM changes, and re-run effects, which is wasteful if the data could have been derived during the initial render or calculated from props [1][3]. Recommended alternatives: - Derive state during render: If the state is based on props or existing state, calculate the value directly in the component body instead of storing it in state [1][3]. - Use useReducer: For complex state transitions or when multiple state updates are related, useReducer can consolidate these into a single dispatch, avoiding multiple render triggers [4]. - Use initial state: If you are setting state based on a prop, consider initializing the state with that value or ensuring the component design does not require synchronization [1][2]. Exceptions and limitations: - Asynchronous updates: Calling setState inside an asynchronous function (e.g., after an await) is generally permitted because it does not trigger a synchronous cascading render [5][3]. - Ref-guarded updates: Updating state based on measurements (like using refs with useLayoutEffect) is often valid [6][2]. Note that the rule has faced challenges with false positives [6][5], and maintainers have been working to improve its ability to distinguish between problematic synchronous patterns and necessary use cases [6][5]. If you encounter this error, evaluate if the state update is truly necessary or if it can be achieved by refactoring your component to derive the value during render [1][4][2]. If you believe the warning is a false positive, verify that your state update is indeed asynchronous or necessary for a legitimate side effect that cannot be handled via derivation [6][5][3].

Citations:


Remove synchronous state updates from these effects. Both effects call setters synchronously, which is flagged by react-hooks/set-state-in-effect.

  • src/components/dashboard/leads-table.tsx#L386-L390: derive the SSR-prop sync values instead of updating localLeads and total in this effect.
  • src/components/dashboard/leads-table.tsx#L704-L708: derive cleared facet values when facetFetchParams is null instead of clearing both facet states in the effect.
🧰 Tools
🪛 ESLint

[error] 388-388: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/src/components/dashboard/leads-table.tsx:388:5
386 | useEffect(() => {
387 | if (advancedFilterActive) return;

388 | setLocalLeads(leads);
| ^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
389 | setTotal(initialTotal ?? leads.length);
390 | }, [leads, initialTotal, advancedFilterActive]);
391 |

(react-hooks/set-state-in-effect)

📍 Affects 1 file
  • src/components/dashboard/leads-table.tsx#L386-L390 (this comment)
  • src/components/dashboard/leads-table.tsx#L704-L708
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/leads-table.tsx` around lines 386 - 390, Remove
synchronous setters from the effects in LeadsTable: at
src/components/dashboard/leads-table.tsx lines 386-390, derive the SSR-prop
values for localLeads and total during render while preserving
advancedFilterActive behavior; at lines 704-708, derive empty facet values when
facetFetchParams is null instead of clearing facet state in the effect. Update
the relevant reads to use these derived values and retain effect-driven updates
only where asynchronous side effects are required.

Source: Linters/SAST tools

Comment on lines +1928 to +1970
const advancedVisibleFields = useMemo(
() => Object.values(advancedFilterRegistry).filter((f) => !advancedVisibleFieldKeys.has(f.key)),
[advancedFilterRegistry, advancedVisibleFieldKeys]
);

// Reuses the exact option arrays the legacy filterDefs above compute — same
// counselors/sources/forms/tags lists, just fed to a different UI. ADVANCED-FILTERS-
// BRIEF Phase 3 addendum §A: assignees/collaborators must carry counts AND hide
// zero-count people, matching the legacy filterDefs' "counselor"/"collaborator"
// entries above byte-for-byte — the bar must not ship with a visible count
// regression against the toolbar it replaces.
const advancedFilterOptionOverrides: Partial<Record<string, FilterOption[]>> = useMemo(
() => ({
status: statusFilterOptions,
source: sources.map((s) => ({ value: s, label: `${s} (${(sourceCounts.get(s) ?? 0).toLocaleString()})` })),
assignees: [
...((counselorCounts.get("unassigned") ?? 0) > 0
? [{ value: "unassigned", label: `Unassigned (${(counselorCounts.get("unassigned") ?? 0).toLocaleString()})` }]
: []),
...counselors
.filter(([userId]) => (counselorCounts.get(userId) ?? 0) > 0)
.map(([userId, email]) => ({
value: userId,
label: `${memberNames[userId] || email.split("@")[0]} (${(counselorCounts.get(userId) ?? 0).toLocaleString()})`,
})),
],
// Collaborator counts stay client-side, computed from `localLeads` (the current
// server page only) — deliberately, not silently: `lead_collaborators` is a join
// table with no existing `lead_aggregates()` dimension, and adding one is a
// bigger change than this addendum covers (ADVANCED-FILTERS-BRIEF Phase 3
// addendum §C). Being explicitly page-scoped-and-labelled beats shipping a
// second, silently different meaning of the same-looking number.
collaborators: counselors
.filter(([userId]) => (collaboratorCounts.get(userId) ?? 0) > 0 && memberRoleMap[userId] !== "owner" && memberRoleMap[userId] !== "admin")
.map(([userId, email]) => ({
value: userId,
label: `${memberNames[userId] || email.split("@")[0]} (${(collaboratorCounts.get(userId) ?? 0).toLocaleString()})`,
})),
tags: [{ value: "student", label: "Student" }],
industry: PROSPECT_INDUSTRIES.map((ind) => ({ value: ind.value, label: ind.label })),
form: formEntries.map(([id, name]) => ({ value: id, label: name })),
}),
[statusFilterOptions, sources, sourceCounts, counselorCounts, counselors, memberNames, collaboratorCounts, memberRoleMap, formEntries]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not expose destinations without option data.

advancedVisibleFields includes the registry's destinations multiselect field. It has no static options, and advancedFilterOptionOverrides does not provide one. useFilterOptions therefore returns an empty list and the value editor has no selectable values.

Provide an override or async loader for destinations, or hide the field until its values are available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/leads-table.tsx` around lines 1928 - 1970, The
advanced filter registry currently exposes the destinations field without
selectable options. Update the advancedFilterOptionOverrides or the
corresponding useFilterOptions configuration to supply destinations values
through an existing override or async loader, and ensure advancedVisibleFields
does not include destinations while those values are unavailable.

Comment on lines +79 to +84
export const RELATIVE_DATE_PRESETS = [
{ value: "1d", label: "Today" },
{ value: "7d", label: "Last 7 days" },
{ value: "30d", label: "Last 30 days" },
{ value: "1m", label: "This month" },
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/lib/filters --items all --type function
rg -n -C 3 'within_last|within_next|"1d"|"1m"|"3m"|"1y"|RELATIVE_DATE' \
  src/lib/filters src/components/filters

Repository: Zunkireelabs/edgexcrm

Length of output: 18362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/components/filters/condition-defaults.ts --items all --type function
sed -n '1,120p' src/components/filters/condition-defaults.ts
sed -n '1,70p' src/components/filters/filter-value-editor.tsx
sed -n '1,35p' src/components/filters/chip-label.ts
sed -n '220,250p' src/lib/filters/compile.ts
python3 - <<'PY'
from pathlib import Path
p = Path("src/components/filters/condition-defaults.ts").read_text()
start = p.index("export const RELATIVE_DATE_PRESETS = [")
end = p.index("];", start) + 2
block = p[start:end]
print(block)

# Simple parse objects { value: "x", label: "y" }
import re
values = []
for m in re.finditer(r'\{\s*value:\s*"([^"]+)"', block):
    label = next((x.group(1) for x in re.finditer(r'label:\s*"([^"]+)"', block[m.start():]), None))
    label = re.search(r'label:\s*"([^"]+)"', block[m.start():]).group(1) if label is None else label
    values.append((m.group(1), label))
print("present_presets=", values)

compile = Path("src/lib/filters/compile.ts").read_text()
start = compile.index('case "within_last":')
end = compile.index('case "within_next":', start)
last = compile[start:end]
next_ = compile[end:end+200]
print("within_last_branch_contains=", f'subtractRelativeWindow({last})' in last)
print("within_next_branch_contains=", f'addRelativeWindow({next_})' in next_)
PY

Repository: Zunkireelabs/edgexcrm

Length of output: 10423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- condition-defaults.ts ---\n'
sed -n '1,110p' src/components/filters/condition-defaults.ts

printf '\n--- filter-value-editor.tsx ---\n'
sed -n '1,75p' src/components/filters/filter-value-editor.tsx

printf '\n--- chip-label.ts ---\n'
sed -n '1,40p' src/components/filters/chip-label.ts

printf '\n--- compile.ts relative window helpers ---\n'
rg -n -C 4 'function (add|subtract)RelativeWindow|dayBoundsInTz|localMidnightUtc|nextCalendarDate' src/lib/filters/compile.ts

printf '\n--- parse preset values and compute window bounds from source definitions ---\n'
python3 - <<'PY'
from pathlib import Path
import re

compile = Path("src/lib/filters/compile.ts").read_text()

# Extract window branch implementations as readable source snippets
m = re.search(r'function (add|subtract)RelativeWindow\(.*?\n\}[ \n]*\n\}', compile, re.S)
if not m:
    raise SystemExit("no window function found")
fn = m.group(0)
print(fn)

start = compile.index('function addRelativeWindow')
end = compile.index('\n}', start) + 2
snippet = compile[start:end]
print("uses_addLocalMidnight=", "addLocalMidnight" in snippet)
print("uses_addRelativeWindow=", "addRelativeWindow" in snippet)
print("adds_duration_units=", bool(re.search(r'(?:milliseconds|minutes|hours|days|weeks|months|years)\s*%', snippet)))

# Locate relative date presets
value_label = {}
for m in re.finditer(r'\{\s*value:\s*"([^"]+)"[\s\S]{0,200}?label:\s*"([^"]+)"', compile[compile.index('RELATIVE_DATE_PRESETS'):]):
    value_label[m.group(1)] = m.group(2)
print("value_label_in_compile=", value_label)
PY

Repository: Zunkireelabs/edgexcrm

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- condition-defaults.ts ---'
sed -n '1,110p' src/components/filters/condition-defaults.ts

echo
echo '--- filter-value-editor.tsx ---'
sed -n '1,75p' src/components/filters/filter-value-editor.tsx

echo
echo '--- chip-label.ts ---'
sed -n '1,40p' src/components/filters/chip-label.ts

echo
echo '--- compile.ts window helpers ---'
rg -n -C 4 'function (add|subtract)RelativeWindow|dayBoundsInTz|localMidnightUtc|nextCalendarDate' src/lib/filters/compile.ts

echo
echo '--- window implementation source ---'
python3 - <<'PY'
from pathlib import Path
import re
text = Path("src/lib/filters/compile.ts").read_text()
fn_name = "addRelativeWindow"
start = text.index(f"function {fn_name}")
brace = text.index("{", start)
depth = 0
end = None
for i in range(brace, len(text)):
    if text[i] == "{":
        depth += 1
    elif text[i] == "}":
        depth -= 1
        if depth == 0:
            end = i + 1
            break
print(text[start:end])
PY

Repository: Zunkireelabs/edgexcrm

Length of output: 12847


Use direction-specific presets that match the documented relative-date values.

filter-value-editor.tsx passes the same presets into both within_last and within_next, while chip-label.ts applies Last ... / This month labels to within_next. Split the preset maps by operator direction and include the documented values from src/lib/filters/tree-to-aggregate-params.ts (7d, 30d, 3m, 1y) instead of adding undocumented 1d and 1m values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/filters/condition-defaults.ts` around lines 79 - 84, Replace
RELATIVE_DATE_PRESETS with direction-specific preset maps for within_last and
within_next, using the documented values 7d, 30d, 3m, and 1y from
tree-to-aggregate-params.ts. Update filter-value-editor.tsx to pass the
appropriate map per operator and update chip-label.ts to use direction-correct
labels, avoiding undocumented 1d and 1m presets.

Comment on lines +114 to +129
if (op === "between") {
const [min, max] = (Array.isArray(value) ? value : [0, 0]) as [number, number];
return (
<div className="flex items-center gap-1.5 p-1">
<Input type="number" className="h-8 w-20 text-xs" value={min} onChange={(e) => onChange([Number(e.target.value), max])} />
<span className="text-xs text-muted-foreground">and</span>
<Input type="number" className="h-8 w-20 text-xs" value={max} onChange={(e) => onChange([min, Number(e.target.value)])} />
</div>
);
}

if (field.type === "number") {
return (
<div className="p-1">
<Input type="number" className="h-8 w-28 text-xs" value={typeof value === "number" ? value : ""} onChange={(e) => onChange(Number(e.target.value))} />
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not coerce an empty numeric input to zero.

Number(e.target.value) converts an empty input to 0. If a user clears a numeric condition, the controlled value immediately becomes zero and applying the condition filters for zero instead of an empty value.

Preserve the empty draft value until validation or apply handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/filters/filter-value-editor.tsx` around lines 114 - 129,
Update the numeric onChange handlers in the between-range and field.type ===
"number" branches of the filter value editor so clearing an input preserves an
empty draft value instead of converting it to 0. Pass the raw input value or the
component’s established empty-value representation through onChange, while
retaining numeric values for non-empty input and existing range behavior.

Comment on lines +63 to +72
it("decodeFilterTree also works with no global Buffer, server-encoded input included", () => {
// Encode with Buffer present (server-side path), then decode with it absent —
// both directions of the "browser encodes, server decodes" contract must hold,
// and the codec must be identical either way (not two divergent implementations).
globalThis.Buffer = originalBuffer;
const tree: FilterTree = { conjunction: "and", conditions: [{ id: "c1", field: "status", op: "is", value: "new" }] };
const encoded = encodeFilterTree(tree);
// @ts-expect-error — re-simulate the browser gap for the decode half.
delete globalThis.Buffer;
expect(decodeFilterTree(encoded)).toEqual({ ok: true, tree });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test actual legacy server encoding.

Line 69 still calls encodeFilterTree, which always uses the new browser-safe codec. Restoring Buffer does not test decoding URLs produced by the previous Buffer.from(...).toString("base64url") implementation.

Build the encoded value with originalBuffer before deleting globalThis.Buffer.

Proposed test correction
-    globalThis.Buffer = originalBuffer;
     const tree: FilterTree = { conjunction: "and", conditions: [{ id: "c1", field: "status", op: "is", value: "new" }] };
-    const encoded = encodeFilterTree(tree);
+    const encoded = originalBuffer
+      .from(JSON.stringify(tree), "utf8")
+      .toString("base64url");
     // `@ts-expect-error` — re-simulate the browser gap for the decode half.
     delete globalThis.Buffer;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("decodeFilterTree also works with no global Buffer, server-encoded input included", () => {
// Encode with Buffer present (server-side path), then decode with it absent —
// both directions of the "browser encodes, server decodes" contract must hold,
// and the codec must be identical either way (not two divergent implementations).
globalThis.Buffer = originalBuffer;
const tree: FilterTree = { conjunction: "and", conditions: [{ id: "c1", field: "status", op: "is", value: "new" }] };
const encoded = encodeFilterTree(tree);
// @ts-expect-error — re-simulate the browser gap for the decode half.
delete globalThis.Buffer;
expect(decodeFilterTree(encoded)).toEqual({ ok: true, tree });
it("decodeFilterTree also works with no global Buffer, server-encoded input included", () => {
// Encode with Buffer present (server-side path), then decode with it absent —
// both directions of the "browser encodes, server decodes" contract must hold,
// and the codec must be identical either way (not two divergent implementations).
const tree: FilterTree = { conjunction: "and", conditions: [{ id: "c1", field: "status", op: "is", value: "new" }] };
const encoded = originalBuffer
.from(JSON.stringify(tree), "utf8")
.toString("base64url");
// `@ts-expect-error` — re-simulate the browser gap for the decode half.
delete globalThis.Buffer;
expect(decodeFilterTree(encoded)).toEqual({ ok: true, tree });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/filters/serialize.browser.test.ts` around lines 63 - 72, Update the
test case around encodeFilterTree so the encoded input is produced directly with
originalBuffer using the legacy server-side Buffer base64url encoding, rather
than calling encodeFilterTree. Keep the subsequent deletion of globalThis.Buffer
and decodeFilterTree assertion unchanged.

Comment on lines +30 to +34
const result = new Date(now);
if (unit === "d") result.setDate(result.getDate() - amount);
else if (unit === "m") result.setMonth(result.getMonth() - amount);
else result.setFullYear(result.getFullYear() - amount);
return result;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '^src/lib/filters/tree-to-aggregate-params\.ts$|^src/app/\(main\)/api/v1/leads/route\.ts$' || true

echo "== file outline =="
ast-grep outline src/lib/filters/tree-to-aggregate-params.ts || true

echo "== file contents =="
cat -n src/lib/filters/tree-to-aggregate-params.ts

echo "== JS Date behavior probe =="
node - <<'JS'
const cases = [
  ["March 31 minus 1m", "2020-03-31", "d", 1],
  ["Feb 29 minus 1y", "2020-02-29", "y", 1],
  ["March 31 minus 31d", "2020-03-31", "d", 31],
];
for (const [label, iso, unit, amount] of cases) {
  const now = new Date(iso);
  const result = new Date(now);
  if (unit === "d") result.setDate(result.getDate() - amount);
  else if (unit === "m") result.setMonth(result.getMonth() - amount);
  else result.setFullYear(result.getFullYear() - amount);
  console.log(label, iso, "=>", result.toISOString().slice(0,10), result.toString());
}
JS

echo "== usages of tree-to-aggregate-params =="
rg -n "tree-to-aggregate-params|within_last|createdAfter" src/lib src/app -S || true

Repository: Zunkireelabs/edgexcrm

Length of output: 15063


Clamp calendar month and year subtraction.

When now is a date that does not exist in the target month, setMonth() and setFullYear() roll forward instead of keeping the target calendar boundary. For example, 2020-03-31T... 1m becomes 2020-03-30T..., so createdAfter excludes a valid last-day record. Use clamped calendar subtraction for m and y, and cover leap-day and last-day edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/filters/tree-to-aggregate-params.ts` around lines 30 - 34, Update the
date subtraction helper containing the `unit` branch so month (`"m"`) and year
(`"y"`) subtraction clamps to the target calendar month’s last valid day instead
of allowing `setMonth()` or `setFullYear()` rollover; preserve day subtraction
and time components, and cover leap-day and month-end cases in tests.

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