Skip to content

[schemas] Add crm-person-tiers — relationship tier schema + dashboard page#18

Open
alanshurafa wants to merge 5 commits intomainfrom
contrib/alanshurafa/crm-person-tiers
Open

[schemas] Add crm-person-tiers — relationship tier schema + dashboard page#18
alanshurafa wants to merge 5 commits intomainfrom
contrib/alanshurafa/crm-person-tiers

Conversation

@alanshurafa
Copy link
Copy Markdown
Owner

Summary

Adds a new schemas/crm-person-tiers/ contribution, ported from Alan's ExoCortex repo (migration 202604210007_crm_person_tiers.sql + web/extensions/crm/page.tsx), generalized for the public OB1 contract.

  • crm_persons table — standalone person record with a four-value relationship_tier CHECK constraint (connected > contact > known > unknown), case-insensitive unique index on canonical_name, and an auto-touch updated_at trigger.
  • crm_person_mentions join table — links persons to core thoughts rows without modifying the thoughts table (OB1 contribution rule).
  • crm_person_tiers(...) RPC — paginated view that returns each person's stored tier plus an effective_tier that promotes high-activity recent contacts to connected. Knobs (p_promote_min_mentions, p_promote_within) are RPC parameters so callers can retune per request.
  • Optional dashboard snippet (dashboard-snippets/page.tsx + README) — drop-in Next.js App Router page that renders the RPC output with tier badges. ExoCortex-specific imports (@/lib/api, @/lib/auth, @/lib/extensions/types) stripped; only a generic getSupabaseServerClient() helper is assumed.

Prereq assumption

The ExoCortex source is tightly coupled to a separate entities + thought_entities graph that OB1 doesn't have. Rather than drag that graph in, I made the schema standalone: a crm_persons table with relationship_tier, plus a crm_person_mentions join table that references thoughts.id as UUID (the default from docs/01-getting-started.md). README flags the BIGINT alternative.

Safety

  • Idempotent: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, CREATE OR REPLACE FUNCTION.
  • No DROP TABLE, DROP DATABASE, TRUNCATE, or unqualified DELETE FROM.
  • Core thoughts table is untouched.
  • No credentials or personal data in examples (uses placeholder name "Alex Rivera").

Test plan

  • Markdownlint clean on new files (markdownlint-cli2 --config .github/.markdownlint.jsonc "schemas/crm-person-tiers/**/*.md" → 0 errors)
  • metadata.json parses and matches the required schema fields
  • OB1 gate SQL-safety regex (DROP\s+TABLE|DROP\s+DATABASE|TRUNCATE) matches zero lines in schema.sql
  • OB1 gate ALTER thoughts ... COLUMN regex matches zero lines
  • Run schema.sql against a test Supabase database and confirm tables + RPC are created
  • Insert a test person and call crm_person_tiers(p_limit := 5) to verify tier rollup
  • (Optional) Drop dashboard-snippets/page.tsx into dashboards/open-brain-dashboard-next and confirm the /crm route renders

Review status

Fork PR only. Pre-review pending — will run gsd-code-reviewer (Claude) + codex exec review (Codex) before considering an upstream PR to NateBJones-Projects/OB1, per Alan's cross-AI review pattern.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab49c88f4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +115 to +117
const thoughtId = active.id as number;
const newStatus = over.id as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive drop target status from container, not over.id

handleDragEnd treats over.id as the destination status, but in a SortableContext this is often the hovered card's id rather than the column id. In that common case, a drop over another card sends a numeric status to /api/kanban/update, fails server validation (new|planning|active|review|done|archived), and triggers the optimistic revert path, so drag-and-drop appears broken unless the user lands on empty column space. Resolve the target status from the droppable/sortable container metadata instead of over.id directly.

Useful? React with 👍 / 👎.

Comment on lines +107 to +110
for (const thoughtType of ["task", "idea"]) {
const sp = new URLSearchParams();
sp.set("per_page", "100");
sp.set("sort", "importance");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Paginate kanban fetches beyond first 100 items per type

The kanban data loader fetches exactly one page per thought type with per_page=100 and never requests subsequent pages. Accounts with more than 100 task or more than 100 idea thoughts will silently miss the rest on the board, which means those items cannot be reviewed or updated from this workflow UI. Add paging (or loop until a short page) so the board reflects the full dataset.

Useful? React with 👍 / 👎.

…VOKER

The public crm_person_tiers RPC was declared SECURITY DEFINER with
EXECUTE granted to anon. An exposed Supabase anon key would let any
caller dump every person's canonical_name, aliases, and metadata from
crm_persons -- no RLS is enabled and SECURITY DEFINER bypasses table
grants.

Least-privilege fix: function is now SECURITY INVOKER and execute is
granted to authenticated + service_role only. Added README "Security"
section documenting the intended install path (server-side service_role
or authenticated-with-RLS) and how to opt into anon access on purpose.

Flagged by Claude gsd-code-reviewer + Codex review.
The crm_person_tiers RPC applied LIMIT/OFFSET inside person_page CTE
before mention_count and effective_tier were computed. The final
ORDER BY effective_tier priority then only re-sorted the preselected
page -- for datasets above p_limit, page 1 could miss high-mention
"connected" people that sort later by last_seen, contradicting the
documented ordering contract (tier priority first).

Restructured the CTE chain to: filter -> aggregate mention_counts ->
compute effective_tier -> global ORDER BY -> LIMIT/OFFSET. Added an
inline comment explaining why ordering must precede pagination.

Flagged by Claude gsd-code-reviewer + Codex review.
Two dashboard snippet fixes:

P2 - per-tier summary strip derived counts from the first 400 loaded
rows while the adjacent caption showed total persons, making the strip
look like a global breakdown. Caption now reads "N loaded (of M total
-- per-tier counts above reflect loaded rows only, capped at 400)"
when total > loaded. No aggregate RPC required.

P3 - server component threw on RPC failure, triggering Next.js's
default error surface for drop-in consumers that lack a route-level
error boundary. fetchPersonTiers now returns a typed discriminated
union; CrmPage renders an in-page rose error panel with a troubleshoot
hint (run NOTIFY pgrst, 'reload schema') and logs detail server-side.

Flagged by Claude gsd-code-reviewer + Codex review.
@alanshurafa
Copy link
Copy Markdown
Owner Author

Refreshing checks after markdownlint cleanup merged into fork main.

@alanshurafa alanshurafa reopened this Apr 22, 2026
@alanshurafa
Copy link
Copy Markdown
Owner Author

Refreshing checks after fork markdownlint workflow fix.

@alanshurafa alanshurafa reopened this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant