From 56a743fb325844f6059cd894b7d788eaa2e02038 Mon Sep 17 00:00:00 2001 From: Christian Galo Date: Sat, 25 Jul 2026 15:44:21 -0500 Subject: [PATCH] Add overview counts and system-health queries Back the operator landing surface with live deployment counts and integration health signals, one sqlc query per owning module. CountDeliveringGrants joins core.pool_provisions rather than filtering grants.status: grants.status is an issuance ledger recording what was written down and whether it was later revoked, not whether service is flowing. The current-delivery fact lives on pool_provisions.status, and a grant can sit at status='active' with every provision ended. Counting the ledger alone would overstate delivery. CountClaimsByLifecycle and CountOutboxByStatus each return their buckets in a single row, so the halves that get printed together are read at the same instant and the landing surface pays one round trip rather than one per bucket. --- internal/billing/products.sql.go | 15 ++++++ internal/billing/querier.go | 8 ++++ internal/billing/queries/products.sql | 7 +++ internal/billing/queries/subscriptions.sql | 7 +++ internal/billing/subscriptions.sql.go | 15 ++++++ internal/domains/claims.sql.go | 23 ++++++++++ internal/domains/querier.go | 5 ++ internal/domains/queries/claims.sql | 10 ++++ internal/entitlements/grants.sql.go | 23 ++++++++++ internal/entitlements/pool_provisions.sql.go | 16 +++++++ internal/entitlements/querier.go | 16 +++++++ internal/entitlements/queries/grants.sql | 15 ++++++ .../entitlements/queries/pool_provisions.sql | 8 ++++ internal/identity/persons.sql.go | 15 ++++++ internal/identity/querier.go | 4 ++ internal/identity/queries/persons.sql | 7 +++ internal/integration/outbox.sql.go | 46 +++++++++++++++++++ internal/integration/querier.go | 15 ++++++ internal/integration/queries/outbox.sql | 20 ++++++++ internal/organization/organizations.sql.go | 14 ++++++ internal/organization/querier.go | 3 ++ .../organization/queries/organizations.sql | 6 +++ 22 files changed, 298 insertions(+) create mode 100644 internal/integration/outbox.sql.go create mode 100644 internal/integration/queries/outbox.sql diff --git a/internal/billing/products.sql.go b/internal/billing/products.sql.go index 81cdd53..f2b679b 100644 --- a/internal/billing/products.sql.go +++ b/internal/billing/products.sql.go @@ -13,6 +13,21 @@ import ( "github.com/sqlc-dev/pqtype" ) +const countPublishedProducts = `-- name: CountPublishedProducts :one +SELECT COUNT(*) FROM core.products +WHERE lifecycle_status = 'published' +` + +// Operator overview tile: the size of the live catalog. Counts only +// lifecycle_status='published' — drafts are work in progress and retired +// products are history, so neither belongs in a "what we sell today" number. +func (q *Queries) CountPublishedProducts(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countPublishedProducts) + var count int64 + err := row.Scan(&count) + return count, err +} + const createProduct = `-- name: CreateProduct :one INSERT INTO core.products (name, description, display_category, is_active, is_public, entitlement_set_id, lifecycle_status) VALUES ($1, $2, $3, $4, $5, $6, $7) diff --git a/internal/billing/querier.go b/internal/billing/querier.go index 71d5e6a..af098ba 100644 --- a/internal/billing/querier.go +++ b/internal/billing/querier.go @@ -18,6 +18,14 @@ type Querier interface { // MarkDefaultPrice (clear before set, so the partial unique index never sees // two defaults for one product). ClearDefaultPrice(ctx context.Context, productID string) error + // Operator overview tile. 'active' and 'trialing' are the two statuses that + // mean the subscription is presently owed service — the same pair + // operator_billing.go treats as live when it renders a subscription badge. + CountLiveSubscriptions(ctx context.Context) (int64, error) + // Operator overview tile: the size of the live catalog. Counts only + // lifecycle_status='published' — drafts are work in progress and retired + // products are history, so neither belongs in a "what we sell today" number. + CountPublishedProducts(ctx context.Context) (int64, error) CreateBillingAccount(ctx context.Context, arg CreateBillingAccountParams) (Account, error) CreateInvoice(ctx context.Context, arg CreateInvoiceParams) (Invoice, error) CreateInvoiceLineItem(ctx context.Context, arg CreateInvoiceLineItemParams) (InvoiceLineItem, error) diff --git a/internal/billing/queries/products.sql b/internal/billing/queries/products.sql index f43c85b..4413ed6 100644 --- a/internal/billing/queries/products.sql +++ b/internal/billing/queries/products.sql @@ -42,6 +42,13 @@ SET name = $2, description = $3, display_category = $4, is_active = $5, is_publi WHERE product_id = $1 RETURNING *; +-- name: CountPublishedProducts :one +-- Operator overview tile: the size of the live catalog. Counts only +-- lifecycle_status='published' — drafts are work in progress and retired +-- products are history, so neither belongs in a "what we sell today" number. +SELECT COUNT(*) FROM core.products +WHERE lifecycle_status = 'published'; + -- name: GetProductShape :one -- Diagnostics view (migration 00006): independent structural dimensions per -- product. Readiness reads set_present and billing_shape. diff --git a/internal/billing/queries/subscriptions.sql b/internal/billing/queries/subscriptions.sql index e976e12..c65568d 100644 --- a/internal/billing/queries/subscriptions.sql +++ b/internal/billing/queries/subscriptions.sql @@ -39,3 +39,10 @@ SELECT FROM core.subscriptions s JOIN core.accounts ba ON s.billing_account_id = ba.billing_account_id ORDER BY s.created_at DESC; + +-- name: CountLiveSubscriptions :one +-- Operator overview tile. 'active' and 'trialing' are the two statuses that +-- mean the subscription is presently owed service — the same pair +-- operator_billing.go treats as live when it renders a subscription badge. +SELECT COUNT(*) FROM core.subscriptions +WHERE status IN ('active', 'trialing'); diff --git a/internal/billing/subscriptions.sql.go b/internal/billing/subscriptions.sql.go index 9a972d2..13735f0 100644 --- a/internal/billing/subscriptions.sql.go +++ b/internal/billing/subscriptions.sql.go @@ -11,6 +11,21 @@ import ( "time" ) +const countLiveSubscriptions = `-- name: CountLiveSubscriptions :one +SELECT COUNT(*) FROM core.subscriptions +WHERE status IN ('active', 'trialing') +` + +// Operator overview tile. 'active' and 'trialing' are the two statuses that +// mean the subscription is presently owed service — the same pair +// operator_billing.go treats as live when it renders a subscription badge. +func (q *Queries) CountLiveSubscriptions(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countLiveSubscriptions) + var count int64 + err := row.Scan(&count) + return count, err +} + const createSubscription = `-- name: CreateSubscription :one INSERT INTO core.subscriptions (billing_account_id, status, current_period_start, current_period_end) VALUES ($1, $2, $3, $4) diff --git a/internal/domains/claims.sql.go b/internal/domains/claims.sql.go index bee49c5..0940643 100644 --- a/internal/domains/claims.sql.go +++ b/internal/domains/claims.sql.go @@ -14,6 +14,29 @@ import ( "github.com/lib/pq" ) +const countClaimsByLifecycle = `-- name: CountClaimsByLifecycle :one +SELECT + COUNT(*) FILTER (WHERE status = 'active') AS live_count, + COUNT(*) FILTER (WHERE status = 'pending') AS pending_count +FROM domains.claims +` + +type CountClaimsByLifecycleRow struct { + LiveCount int64 `json:"live_count"` + PendingCount int64 `json:"pending_count"` +} + +// Operator overview tile, deployment-wide. Both halves come back in one round +// trip because the tile prints them together ("N live · M awaiting +// verification") and they must be read at the same instant to stay coherent. +// Terminal statuses (expired/canceled/released) are history and are excluded. +func (q *Queries) CountClaimsByLifecycle(ctx context.Context) (CountClaimsByLifecycleRow, error) { + row := q.db.QueryRowContext(ctx, countClaimsByLifecycle) + var i CountClaimsByLifecycleRow + err := row.Scan(&i.LiveCount, &i.PendingCount) + return i, err +} + const countPendingClaimsByWorkspace = `-- name: CountPendingClaimsByWorkspace :one SELECT COUNT(*) FROM domains.claims WHERE workspace_id = $1 AND status = 'pending' diff --git a/internal/domains/querier.go b/internal/domains/querier.go index f710e43..8f6e40b 100644 --- a/internal/domains/querier.go +++ b/internal/domains/querier.go @@ -11,6 +11,11 @@ import ( ) type Querier interface { + // Operator overview tile, deployment-wide. Both halves come back in one round + // trip because the tile prints them together ("N live · M awaiting + // verification") and they must be read at the same instant to stay coherent. + // Terminal statuses (expired/canceled/released) are history and are excluded. + CountClaimsByLifecycle(ctx context.Context) (CountClaimsByLifecycleRow, error) // Soft cap on concurrent external verifications — each is a live 24-hour // polling workflow. CountPendingClaimsByWorkspace(ctx context.Context, workspaceID string) (int64, error) diff --git a/internal/domains/queries/claims.sql b/internal/domains/queries/claims.sql index ccb89f1..d6cb35a 100644 --- a/internal/domains/queries/claims.sql +++ b/internal/domains/queries/claims.sql @@ -65,6 +65,16 @@ JOIN core.organizations o ON o.org_id = w.org_id WHERE c.status IN ('pending', 'active') ORDER BY (c.status = 'pending') DESC, c.created_at DESC; +-- name: CountClaimsByLifecycle :one +-- Operator overview tile, deployment-wide. Both halves come back in one round +-- trip because the tile prints them together ("N live · M awaiting +-- verification") and they must be read at the same instant to stay coherent. +-- Terminal statuses (expired/canceled/released) are history and are excluded. +SELECT + COUNT(*) FILTER (WHERE status = 'active') AS live_count, + COUNT(*) FILTER (WHERE status = 'pending') AS pending_count +FROM domains.claims; + -- name: CountPendingClaimsByWorkspace :one -- Soft cap on concurrent external verifications — each is a live 24-hour -- polling workflow. diff --git a/internal/entitlements/grants.sql.go b/internal/entitlements/grants.sql.go index 831625d..67471c1 100644 --- a/internal/entitlements/grants.sql.go +++ b/internal/entitlements/grants.sql.go @@ -14,6 +14,29 @@ import ( "github.com/sqlc-dev/pqtype" ) +const countDeliveringGrants = `-- name: CountDeliveringGrants :one +SELECT COUNT(DISTINCT g.grant_id) FROM core.grants g +JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id +WHERE pp.status = 'active' +` + +// Operator overview tile: grants that are actually delivering entitlement +// right now. +// +// grants.status is an issuance LEDGER — it records what was written and +// whether it was later revoked, not whether service is flowing today. The +// current-delivery fact lives on core.pool_provisions.status, so this counts +// distinct grants that own at least one active provision rather than +// filtering grants.status alone. A grant can sit at status='active' with +// every provision ended (nothing delivering), which is exactly the +// overstatement this join avoids. +func (q *Queries) CountDeliveringGrants(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countDeliveringGrants) + var count int64 + err := row.Scan(&count) + return count, err +} + const createGrant = `-- name: CreateGrant :one INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id, grant_reason, description, quantity, valid_from, valid_until, extends_grant_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) diff --git a/internal/entitlements/pool_provisions.sql.go b/internal/entitlements/pool_provisions.sql.go index a7cf530..1cf865e 100644 --- a/internal/entitlements/pool_provisions.sql.go +++ b/internal/entitlements/pool_provisions.sql.go @@ -11,6 +11,22 @@ import ( "github.com/google/uuid" ) +const countDeliveringProvisions = `-- name: CountDeliveringProvisions :one +SELECT COUNT(*) FROM core.pool_provisions +WHERE status = 'active' +` + +// Operator overview tile: how many pool provisions are delivering right now. +// pool_provisions.status is the authoritative "currently delivering" flag — +// grants.status is an issuance ledger and must not be used for this — so the +// overview counts this table directly. +func (q *Queries) CountDeliveringProvisions(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countDeliveringProvisions) + var count int64 + err := row.Scan(&count) + return count, err +} + const getActivePoolProvisionsByPoolID = `-- name: GetActivePoolProvisionsByPoolID :many SELECT provision_id, pool_id, billing_account_id, subscription_id, purchase_id, grant_id, quantity, status, activated_at, suspended_at, ended_at, created_at, updated_at, entitlement_set_id, product_id FROM core.pool_provisions WHERE pool_id = $1 AND status = 'active' diff --git a/internal/entitlements/querier.go b/internal/entitlements/querier.go index 501d381..76a712c 100644 --- a/internal/entitlements/querier.go +++ b/internal/entitlements/querier.go @@ -16,6 +16,22 @@ type Querier interface { AtomicIncrementUsage(ctx context.Context, arg AtomicIncrementUsageParams) (sql.Result, error) CountActiveAttachmentsByLadder(ctx context.Context, planLadderID string) (int64, error) CountActiveAttachmentsByTier(ctx context.Context, arg CountActiveAttachmentsByTierParams) (int64, error) + // Operator overview tile: grants that are actually delivering entitlement + // right now. + // + // grants.status is an issuance LEDGER — it records what was written and + // whether it was later revoked, not whether service is flowing today. The + // current-delivery fact lives on core.pool_provisions.status, so this counts + // distinct grants that own at least one active provision rather than + // filtering grants.status alone. A grant can sit at status='active' with + // every provision ended (nothing delivering), which is exactly the + // overstatement this join avoids. + CountDeliveringGrants(ctx context.Context) (int64, error) + // Operator overview tile: how many pool provisions are delivering right now. + // pool_provisions.status is the authoritative "currently delivering" flag — + // grants.status is an issuance ledger and must not be used for this — so the + // overview counts this table directly. + CountDeliveringProvisions(ctx context.Context) (int64, error) CreateEntitlementSet(ctx context.Context, arg CreateEntitlementSetParams) (EntitlementSet, error) CreateEntitlementSetRule(ctx context.Context, arg CreateEntitlementSetRuleParams) (EntitlementSetRule, error) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error) diff --git a/internal/entitlements/queries/grants.sql b/internal/entitlements/queries/grants.sql index a5a1abe..2f2ffe0 100644 --- a/internal/entitlements/queries/grants.sql +++ b/internal/entitlements/queries/grants.sql @@ -104,6 +104,21 @@ WHERE granted_to_org_id IS NOT NULL ORDER BY created_at DESC LIMIT $1; +-- name: CountDeliveringGrants :one +-- Operator overview tile: grants that are actually delivering entitlement +-- right now. +-- +-- grants.status is an issuance LEDGER — it records what was written and +-- whether it was later revoked, not whether service is flowing today. The +-- current-delivery fact lives on core.pool_provisions.status, so this counts +-- distinct grants that own at least one active provision rather than +-- filtering grants.status alone. A grant can sit at status='active' with +-- every provision ended (nothing delivering), which is exactly the +-- overstatement this join avoids. +SELECT COUNT(DISTINCT g.grant_id) FROM core.grants g +JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id +WHERE pp.status = 'active'; + -- name: ListAllGrants :many SELECT * FROM core.grants ORDER BY created_at DESC; diff --git a/internal/entitlements/queries/pool_provisions.sql b/internal/entitlements/queries/pool_provisions.sql index d3b754f..8b3a66b 100644 --- a/internal/entitlements/queries/pool_provisions.sql +++ b/internal/entitlements/queries/pool_provisions.sql @@ -31,6 +31,14 @@ SELECT * FROM core.pool_provisions WHERE subscription_id = $1 AND status <> 'ended' ORDER BY created_at ASC; +-- name: CountDeliveringProvisions :one +-- Operator overview tile: how many pool provisions are delivering right now. +-- pool_provisions.status is the authoritative "currently delivering" flag — +-- grants.status is an issuance ledger and must not be used for this — so the +-- overview counts this table directly. +SELECT COUNT(*) FROM core.pool_provisions +WHERE status = 'active'; + -- name: GetLivePoolProvisionsByProductID :many -- Live (non-ended) provisions delivering a product. Tier add/remove aligns -- each of these to the product's new conferral shape. diff --git a/internal/identity/persons.sql.go b/internal/identity/persons.sql.go index 726ba55..4eb9b78 100644 --- a/internal/identity/persons.sql.go +++ b/internal/identity/persons.sql.go @@ -9,6 +9,21 @@ import ( "context" ) +const countActivePersons = `-- name: CountActivePersons :one +SELECT COUNT(*) FROM core.persons +WHERE status = 'active' +` + +// Operator overview tile: how many people the deployment currently serves. +// Mirrors LookupPersons' status='active' filter so the headline number and +// the lookup affordance above it are counting the same population. +func (q *Queries) CountActivePersons(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countActivePersons) + var count int64 + err := row.Scan(&count) + return count, err +} + const createPerson = `-- name: CreatePerson :one INSERT INTO core.persons (user_id, display_name, primary_email, primary_email_verified) VALUES ($1, $2, $3, $4) diff --git a/internal/identity/querier.go b/internal/identity/querier.go index 7edee67..c8a93d8 100644 --- a/internal/identity/querier.go +++ b/internal/identity/querier.go @@ -9,6 +9,10 @@ import ( ) type Querier interface { + // Operator overview tile: how many people the deployment currently serves. + // Mirrors LookupPersons' status='active' filter so the headline number and + // the lookup affordance above it are counting the same population. + CountActivePersons(ctx context.Context) (int64, error) CreatePerson(ctx context.Context, arg CreatePersonParams) (Person, error) CreateUser(ctx context.Context, oidcSubject string) (User, error) // Idempotently insert the reserved system person that owns the System tenant diff --git a/internal/identity/queries/persons.sql b/internal/identity/queries/persons.sql index 3eec392..434bc2c 100644 --- a/internal/identity/queries/persons.sql +++ b/internal/identity/queries/persons.sql @@ -43,3 +43,10 @@ ORDER BY CASE WHEN primary_email = $1 THEN 0 ELSE 1 END, display_name LIMIT 20; + +-- name: CountActivePersons :one +-- Operator overview tile: how many people the deployment currently serves. +-- Mirrors LookupPersons' status='active' filter so the headline number and +-- the lookup affordance above it are counting the same population. +SELECT COUNT(*) FROM core.persons +WHERE status = 'active'; diff --git a/internal/integration/outbox.sql.go b/internal/integration/outbox.sql.go new file mode 100644 index 0000000..f08fa19 --- /dev/null +++ b/internal/integration/outbox.sql.go @@ -0,0 +1,46 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: outbox.sql + +package integration + +import ( + "context" +) + +const countOutboxByStatus = `-- name: CountOutboxByStatus :one +SELECT + COUNT(*) FILTER (WHERE status = 'pending') AS pending_count, + COUNT(*) FILTER (WHERE status = 'failed') AS failed_count, + COUNT(*) FILTER (WHERE status = 'dead_letter') AS dead_letter_count +FROM core.outbox +` + +type CountOutboxByStatusRow struct { + PendingCount int64 `json:"pending_count"` + FailedCount int64 `json:"failed_count"` + DeadLetterCount int64 `json:"dead_letter_count"` +} + +// Operator overview "System" panel: the health of the integration delivery +// queue in one round trip. +// +// The three buckets mean different things to an operator and are therefore +// reported separately rather than summed: +// +// pending — enqueued, not yet attempted or waiting on next_attempt_at. +// A steady non-zero value is normal. +// failed — attempted and errored, still inside max_attempts, so the +// poller will retry. In-flight, not lost. +// dead_letter — exhausted its retries. Nothing will move this without an +// operator, so it is the only bucket that warrants an alarm. +// +// Enqueue-side writes go through integration.Enqueue (internal/integration/ +// outbox.go); this is a read-only health probe over the same table. +func (q *Queries) CountOutboxByStatus(ctx context.Context) (CountOutboxByStatusRow, error) { + row := q.db.QueryRowContext(ctx, countOutboxByStatus) + var i CountOutboxByStatusRow + err := row.Scan(&i.PendingCount, &i.FailedCount, &i.DeadLetterCount) + return i, err +} diff --git a/internal/integration/querier.go b/internal/integration/querier.go index c074f11..b8b6451 100644 --- a/internal/integration/querier.go +++ b/internal/integration/querier.go @@ -11,6 +11,21 @@ import ( type Querier interface { AddProviderOperation(ctx context.Context, arg AddProviderOperationParams) error AddProviderState(ctx context.Context, arg AddProviderStateParams) error + // Operator overview "System" panel: the health of the integration delivery + // queue in one round trip. + // + // The three buckets mean different things to an operator and are therefore + // reported separately rather than summed: + // pending — enqueued, not yet attempted or waiting on next_attempt_at. + // A steady non-zero value is normal. + // failed — attempted and errored, still inside max_attempts, so the + // poller will retry. In-flight, not lost. + // dead_letter — exhausted its retries. Nothing will move this without an + // operator, so it is the only bucket that warrants an alarm. + // + // Enqueue-side writes go through integration.Enqueue (internal/integration/ + // outbox.go); this is a read-only health probe over the same table. + CountOutboxByStatus(ctx context.Context) (CountOutboxByStatusRow, error) DeleteConfigOverride(ctx context.Context, key string) (int64, error) // Boot reconciliation: drop operations a provider no longer declares. DeleteProviderOperationsNotIn(ctx context.Context, arg DeleteProviderOperationsNotInParams) error diff --git a/internal/integration/queries/outbox.sql b/internal/integration/queries/outbox.sql new file mode 100644 index 0000000..7a19e50 --- /dev/null +++ b/internal/integration/queries/outbox.sql @@ -0,0 +1,20 @@ +-- name: CountOutboxByStatus :one +-- Operator overview "System" panel: the health of the integration delivery +-- queue in one round trip. +-- +-- The three buckets mean different things to an operator and are therefore +-- reported separately rather than summed: +-- pending — enqueued, not yet attempted or waiting on next_attempt_at. +-- A steady non-zero value is normal. +-- failed — attempted and errored, still inside max_attempts, so the +-- poller will retry. In-flight, not lost. +-- dead_letter — exhausted its retries. Nothing will move this without an +-- operator, so it is the only bucket that warrants an alarm. +-- +-- Enqueue-side writes go through integration.Enqueue (internal/integration/ +-- outbox.go); this is a read-only health probe over the same table. +SELECT + COUNT(*) FILTER (WHERE status = 'pending') AS pending_count, + COUNT(*) FILTER (WHERE status = 'failed') AS failed_count, + COUNT(*) FILTER (WHERE status = 'dead_letter') AS dead_letter_count +FROM core.outbox; diff --git a/internal/organization/organizations.sql.go b/internal/organization/organizations.sql.go index c665102..96e09fa 100644 --- a/internal/organization/organizations.sql.go +++ b/internal/organization/organizations.sql.go @@ -9,6 +9,20 @@ import ( "context" ) +const countActiveOrganizations = `-- name: CountActiveOrganizations :one +SELECT COUNT(*) FROM core.organizations +WHERE status = 'active' +` + +// Operator overview tile. Counts only status='active' rows so suspended or +// archived organizations don't inflate the headline number. +func (q *Queries) CountActiveOrganizations(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countActiveOrganizations) + var count int64 + err := row.Scan(&count) + return count, err +} + const createOrganization = `-- name: CreateOrganization :one INSERT INTO core.organizations (name, slug, org_type, owner_person_id) VALUES ($1, $2, $3, $4) diff --git a/internal/organization/querier.go b/internal/organization/querier.go index 2d56780..549b82a 100644 --- a/internal/organization/querier.go +++ b/internal/organization/querier.go @@ -9,6 +9,9 @@ import ( ) type Querier interface { + // Operator overview tile. Counts only status='active' rows so suspended or + // archived organizations don't inflate the headline number. + CountActiveOrganizations(ctx context.Context) (int64, error) CountWorkspacesByOrgID(ctx context.Context, orgID string) (int64, error) CreateOrgMember(ctx context.Context, arg CreateOrgMemberParams) (OrgMember, error) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) diff --git a/internal/organization/queries/organizations.sql b/internal/organization/queries/organizations.sql index 3513693..b9de635 100644 --- a/internal/organization/queries/organizations.sql +++ b/internal/organization/queries/organizations.sql @@ -25,6 +25,12 @@ SELECT * FROM core.organizations WHERE org_type = $1 ORDER BY name; +-- name: CountActiveOrganizations :one +-- Operator overview tile. Counts only status='active' rows so suspended or +-- archived organizations don't inflate the headline number. +SELECT COUNT(*) FROM core.organizations +WHERE status = 'active'; + -- name: EnsureSystemOrganization :exec -- Idempotently insert the singleton System organization (keyed on the unique -- slug). Read back with GetOrganizationBySlug.