Skip to content

admin/customers: case-insensitive email search + multi-tier filter + lower(email) index#55

Merged
mastermanas805 merged 1 commit into
masterfrom
feat/admin-customer-search-fresh
May 13, 2026
Merged

admin/customers: case-insensitive email search + multi-tier filter + lower(email) index#55
mastermanas805 merged 1 commit into
masterfrom
feat/admin-customer-search-fresh

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

Wires the dashboard's filter pills + search box to GET /api/v1/admin/customers so the founder can actually narrow the customer view. Three behavior changes on top of Track A (PR #48):

  • Case-insensitive email substring searchq=FOUNDER matches founder@x.com. Handler lowercases both sides; new functional index idx_users_email_lower (migration 023) keeps the lookup cheap as the users table grows.
  • Multi-tier filter?tier=hobby,pro ORs both into WHERE plan_tier IN ('hobby','pro'). Single-tier shape (?tier=pro) is unchanged and still emits = $N so PR admin: Customers endpoints (list/detail/tier-change/issue-promo) gated on ADMIN_EMAILS #48's planner stats remain valid.
  • UI-stable bogus tier handling?tier=platinum now returns an empty list (200) rather than 400. Mixed valid+invalid (?tier=pro,platinum) keeps the valid ones. Filter pills degrade gracefully on stale UI builds / typos instead of error-bannering.

Why now

Track A shipped the endpoint + UI scaffolding (filter pills + search box) but the pills didn't actually filter and the search box was case-sensitive prefix-only. This is the wire-up.

Performance note

Migration 023 adds CREATE INDEX IF NOT EXISTS idx_users_email_lower ON users (lower(email))NOT CONCURRENTLY. The migration runner (internal/db/postgres.go:RunMigrations) executes each .sql file through db.Exec, and CREATE INDEX CONCURRENTLY can't run inside a transaction block; lib/pq's multi-statement batching makes this flaky in practice. At instanode's current users-table size (single-digit-thousand rows max) a blocking CREATE INDEX completes in sub-second — lock window is imperceptible. When we cross ~100k users we should revisit and split this migration to run outside the standard runner. Rationale documented in the migration file's leading comment.

Migration is also numbered 023 as requested in the spec, not 022. The current highest is 021; the gap is intentional (mirrors existing gaps like 003/007).

Test plan

🤖 Generated with Claude Code

…lower(email) index

Wires the dashboard's filter pills (anon/free/hobby/pro/team) to the
admin customers list so the founder can actually narrow the view.
Three behavior changes:

- q=<substr> is case-insensitive both ways: q=FOUNDER matches
  founder@x.com (handler lowercases both sides; lower(email) index
  added in migration 023 to keep the lookup cheap at scale).
- tier=hobby,pro is now valid — comma-joined values OR together via
  WHERE plan_tier IN (...). Single-tier shape (tier=pro) is unchanged
  and still uses = $N so PR #48's planner stats stay valid.
- Bogus tier values (typos, stale UI builds) return an empty list,
  not 400. Multi-tier with mixed valid/invalid keeps only the valid
  ones. UI-stable contract for the filter-pills UI.

Adds 5 new test cases covering substring match, case-insensitivity,
multi-tier OR, all-unknown empty-list, and pagination correctness
across q+limit+offset (regression guard against the $N counter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit f1aeb2a into master May 13, 2026
mastermanas805 added a commit that referenced this pull request Jun 5, 2026
…limit (Task #55)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mastermanas805 added a commit that referenced this pull request Jun 5, 2026
)

* feat(plans): flag-gated per-service resource-count caps (Task #55)

Closes the strict-≥80%-margin hole where only queue_count was capped: a tenant
could create MANY postgres/vector/redis/mongodb/storage resources each at the
per-resource size cap and blow the saturated-COGS bound (Redis binding at
$6.50/GB). Adds a per-tier active-resource COUNT cap per service, enforced like
the existing queue_count A6 block — but FLAG-GATED, default OFF.

Flag: RESOURCE_COUNT_CAPS_ENABLED (config.go, default false). When off, the new
enforceResourceCountCap helper returns immediately and runs NO count query —
zero behavior change, proven inert by TestResourceCountCap_FlagOffIsInert +
TestEnforceResourceCountCap_FlagOffInert_Whitebox. Operator enables after a usage
audit so no current tenant is retroactively over a cap.

Enforcement sites (mirror queue.go's A6 block; 402 + agent_action + metric):
  db.go (postgres), vector.go, cache.go (redis), nosql.go (mongodb), storage.go.
Shared helper: internal/handlers/resource_count_cap.go (one call site per
handler, not a copy-pasted block). Count cap fails CLOSED on a count-query error
when enabled (a cheap indexed COUNT; must not silently bypass a cost cap).

Per-tier numbers (api/plans.yaml; mirrors common defaultYAML, depends on
common#47 which is merged): anon/free=1 each; hobby=2; hobby_plus=3;
pro pg/vec/mongo/storage=5 redis=3; growth=6 redis=3; team pg=5 vec=8 redis=4
mongo=6 storage=6. redis_count is the most conservative line everywhere (binding
COGS). Derived so count×size×unit-COGS ≤ tier 20%-of-price budget per service.

Surfaces (rule 22): /api/v1/capabilities resource_count_limit map;
/api/v1/billing/usage count+count_limit on storage services; openapi.go schemas;
content/llms.txt + instanode-web public/llms.txt (separate PRs).

Metric (rule 25): instant_resource_count_limit_blocked_total{service,team_tier}
(metrics.go). Alert + Prom rule + dashboard tile + catalog row in infra PR.

Tests: registry-iterating flag-on guard (rule 18,
TestResourceCountCap_FlagOnAtLimitRejects) so service N+1 can't ship uncapped;
under-limit pass; whitebox edge branches (unlimited, count-error, nil cfg) →
enforceResourceCountCap 100%; capabilities surface guard; config flag test;
strict_margin guard extended to the new *_count fields. make gate green (the one
unrelated pre-existing failure, models.TestLinkGitHubID, reproduces with this
change stashed and touches no files in this diff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(openapi): regenerate snapshot for resource_count_limit + count_limit (Task #55)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant