feat(crq): dashboard parity functions (migration 16) + recover inc 13/14
Brings CRQ to FleetOps-dashboard parity with INC for the Tickets tab's CRQ sub-tab: - 16_crq_dashboard.sql: tickets.crq_open_sla view (mirror of inc_open_sla, no geog; lat/lng from geom) + reporting.fn_crq_dashboard / fn_crq_search / fn_crq_filter_options (mirrors of the inc functions over tickets.crq) + grants to dashboard_ro/grafana_ro. - Recover the previously un-versioned 13_inc_search_fn.sql + 14_inc_filter_options.sql into the repo (verbatim from the live defs) so a fresh DB rebuilds faithfully; the live ledger already lists them so run_migrations skips them there. Consumed by dashboard_api GET /webhook/crq-dashboard|crq-search|crq-filter-options. Not yet applied to prod (pending go-ahead). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
066d866b90
commit
c980f3edd0
3 changed files with 423 additions and 0 deletions
89
migrations/13_inc_search_fn.sql
Normal file
89
migrations/13_inc_search_fn.sql
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
-- 13_inc_search_fn.sql — fleettickets · INC ticket explorer (search) function
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- reporting.fn_inc_search — ad-hoc ticket lookup by id / engineer / cluster /
|
||||
-- status / state / time, for the FleetOps "Ticket explorer" card. Returns
|
||||
-- { count, truncated, limit, state, rows }. Consumed by dashboard_api
|
||||
-- GET /webhook/inc-search.
|
||||
--
|
||||
-- RECOVERED INTO VERSION CONTROL 2026-06-26: this migration was applied to the live
|
||||
-- DB on 2026-06-19 but the file was never committed. Recovered verbatim from the live
|
||||
-- definition (pg_get_functiondef) so a fresh DB rebuilds faithfully; the live ledger
|
||||
-- already lists it, so run_migrations skips it there. The crq mirror is in 16.
|
||||
-- Idempotent (CREATE OR REPLACE).
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
SET search_path = tickets, public;
|
||||
|
||||
CREATE OR REPLACE FUNCTION reporting.fn_inc_search(
|
||||
p_ticket_id text DEFAULT NULL,
|
||||
p_owner text DEFAULT NULL,
|
||||
p_cluster text DEFAULT NULL,
|
||||
p_status text DEFAULT NULL,
|
||||
p_state text DEFAULT 'closed',
|
||||
p_from timestamptz DEFAULT NULL,
|
||||
p_to timestamptz DEFAULT NULL,
|
||||
p_limit integer DEFAULT 500
|
||||
)
|
||||
RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$
|
||||
DECLARE
|
||||
v_state text := lower(COALESCE(NULLIF(p_state, ''), 'closed'));
|
||||
v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 500), 1), 5000);
|
||||
v_result jsonb;
|
||||
BEGIN
|
||||
p_ticket_id := NULLIF(trim(p_ticket_id), '');
|
||||
p_owner := NULLIF(trim(p_owner), '');
|
||||
p_cluster := NULLIF(p_cluster, '');
|
||||
p_status := NULLIF(p_status, '');
|
||||
|
||||
WITH hits AS (
|
||||
SELECT ticket_id, normalized_status, cluster, region, location_name,
|
||||
initcap(lower(NULLIF(owner, ''))) AS owner, assigned_team,
|
||||
sla_status, mttr, closed_at, created_at_service, is_actionable,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_Y(geom) END AS lat,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_X(geom) END AS lng
|
||||
FROM tickets.inc
|
||||
WHERE (p_ticket_id IS NULL OR ticket_id ILIKE '%' || p_ticket_id || '%')
|
||||
AND (p_owner IS NULL OR lower(owner) LIKE '%' || lower(p_owner) || '%')
|
||||
AND (p_cluster IS NULL OR cluster = p_cluster)
|
||||
AND (p_status IS NULL OR normalized_status = p_status)
|
||||
AND CASE v_state
|
||||
WHEN 'open' THEN COALESCE(is_actionable, false)
|
||||
WHEN 'all' THEN COALESCE(is_actionable, false)
|
||||
OR (closed_at IS NOT NULL
|
||||
AND (p_from IS NULL OR closed_at >= p_from)
|
||||
AND (p_to IS NULL OR closed_at < p_to))
|
||||
ELSE NOT COALESCE(is_actionable, false) -- 'closed'
|
||||
AND closed_at IS NOT NULL
|
||||
AND (p_from IS NULL OR closed_at >= p_from)
|
||||
AND (p_to IS NULL OR closed_at < p_to)
|
||||
END
|
||||
),
|
||||
total AS (SELECT count(*) AS n FROM hits),
|
||||
page AS (
|
||||
SELECT * FROM hits
|
||||
ORDER BY closed_at DESC NULLS LAST, created_at_service DESC NULLS LAST
|
||||
LIMIT v_limit
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'count', (SELECT n FROM total),
|
||||
'truncated', (SELECT n FROM total) > v_limit,
|
||||
'limit', v_limit,
|
||||
'state', v_state,
|
||||
'rows', COALESCE((SELECT jsonb_agg(to_jsonb(page)
|
||||
ORDER BY page.closed_at DESC NULLS LAST,
|
||||
page.created_at_service DESC NULLS LAST)
|
||||
FROM page), '[]'::jsonb)
|
||||
) INTO v_result;
|
||||
|
||||
RETURN v_result;
|
||||
END $function$;
|
||||
|
||||
DO $grants$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_inc_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO dashboard_ro;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_inc_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO grafana_ro;
|
||||
END IF;
|
||||
END $grants$;
|
||||
37
migrations/14_inc_filter_options.sql
Normal file
37
migrations/14_inc_filter_options.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- 14_inc_filter_options.sql — fleettickets · INC explorer dropdown options
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- reporting.fn_inc_filter_options — distinct engineers (owner), clusters, and the
|
||||
-- ids of currently-open tickets, for the FleetOps "Ticket explorer" dropdowns.
|
||||
-- Consumed by dashboard_api GET /webhook/inc-filter-options.
|
||||
--
|
||||
-- RECOVERED INTO VERSION CONTROL 2026-06-26: applied to the live DB 2026-06-19 but
|
||||
-- never committed. Recovered verbatim from the live definition so a fresh DB rebuilds
|
||||
-- faithfully; the live ledger already lists it (run_migrations skips it there). The crq
|
||||
-- mirror is in 16. Idempotent (CREATE OR REPLACE).
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
SET search_path = tickets, public;
|
||||
|
||||
CREATE OR REPLACE FUNCTION reporting.fn_inc_filter_options()
|
||||
RETURNS jsonb LANGUAGE sql STABLE AS $function$
|
||||
SELECT jsonb_build_object(
|
||||
'owners', (SELECT COALESCE(jsonb_agg(o ORDER BY o), '[]'::jsonb)
|
||||
FROM (SELECT DISTINCT initcap(lower(NULLIF(owner, ''))) AS o
|
||||
FROM tickets.inc WHERE NULLIF(owner, '') IS NOT NULL) s),
|
||||
'clusters', (SELECT COALESCE(jsonb_agg(c ORDER BY c), '[]'::jsonb)
|
||||
FROM (SELECT DISTINCT cluster AS c
|
||||
FROM tickets.inc WHERE NULLIF(cluster, '') IS NOT NULL) s),
|
||||
'open_ticket_ids', (SELECT COALESCE(jsonb_agg(ticket_id ORDER BY ticket_id), '[]'::jsonb)
|
||||
FROM tickets.inc WHERE COALESCE(is_actionable, false))
|
||||
);
|
||||
$function$;
|
||||
|
||||
DO $grants$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_inc_filter_options() TO dashboard_ro;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_inc_filter_options() TO grafana_ro;
|
||||
END IF;
|
||||
END $grants$;
|
||||
297
migrations/16_crq_dashboard.sql
Normal file
297
migrations/16_crq_dashboard.sql
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
-- 16_crq_dashboard.sql — fleettickets · CRQ dashboard parity (view + read functions)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Brings CRQ to FleetOps-dashboard parity with INC, so the Tickets tab's CRQ
|
||||
-- sub-tab works "just like INC". Mirrors, against tickets.crq:
|
||||
-- tickets.crq_open_sla ← mirror of tickets.inc_open_sla (08)
|
||||
-- reporting.fn_crq_dashboard ← mirror of reporting.fn_inc_dashboard (09/12)
|
||||
-- reporting.fn_crq_search ← mirror of reporting.fn_inc_search (13)
|
||||
-- reporting.fn_crq_filter_options ← mirror of reporting.fn_inc_filter_options (14)
|
||||
-- consumed by dashboard_api GET /webhook/crq-dashboard | crq-search | crq-filter-options.
|
||||
--
|
||||
-- Differences from the INC view: tickets.crq has no `geog` column (mig 05 is INC-only)
|
||||
-- and its latitude/longitude come from `raw` (empty in the feed), so crq_open_sla omits
|
||||
-- geog and derives latitude/longitude from `geom`. The 48h SLA rule is reused verbatim
|
||||
-- for layout parity — installation-lifecycle SLA semantics may be refined later.
|
||||
--
|
||||
-- Idempotent (CREATE OR REPLACE / VIEW). Requires migration 15 (tickets.crq + typed cols).
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
SET search_path = tickets, public;
|
||||
|
||||
-- ── crq_open_sla — open CRQ tickets with derived SLA (mirror of inc_open_sla) ─
|
||||
CREATE OR REPLACE VIEW tickets.crq_open_sla AS
|
||||
SELECT
|
||||
ticket_id,
|
||||
normalized_status,
|
||||
bucket,
|
||||
cluster,
|
||||
region,
|
||||
location_name,
|
||||
assigned_team,
|
||||
owner,
|
||||
sla_status AS source_sla_status,
|
||||
mttr, -- minutes (null until closed)
|
||||
COALESCE(created_at_service, first_seen_at) AS sla_clock,
|
||||
CASE WHEN created_at_service IS NOT NULL THEN 'service' ELSE 'first_seen' END AS sla_clock_source,
|
||||
round((EXTRACT(EPOCH FROM now() - COALESCE(created_at_service, first_seen_at)) / 3600)::numeric, 1) AS hours_open,
|
||||
CASE
|
||||
WHEN COALESCE(created_at_service, first_seen_at) IS NULL THEN 'unknown'
|
||||
WHEN now() - COALESCE(created_at_service, first_seen_at) >= interval '48h' THEN 'breached'
|
||||
WHEN now() - COALESCE(created_at_service, first_seen_at) >= interval '36h' THEN 'at_risk'
|
||||
ELSE 'ok'
|
||||
END AS sla_state,
|
||||
created_at_service,
|
||||
first_seen_at,
|
||||
scheduled_at,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_Y(geom) END AS latitude,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_X(geom) END AS longitude,
|
||||
geo_source,
|
||||
geom
|
||||
FROM tickets.crq
|
||||
WHERE is_actionable;
|
||||
|
||||
COMMENT ON VIEW tickets.crq_open_sla IS
|
||||
'Open (is_actionable) CRQ tickets with derived SLA (48h rule; clock = created_at_service '
|
||||
'or first_seen_at fallback). Mirror of inc_open_sla; no geog. fleettickets 16.';
|
||||
|
||||
-- ── fn_crq_dashboard — mirror of fn_inc_dashboard over tickets.crq ───────────
|
||||
CREATE OR REPLACE FUNCTION reporting.fn_crq_dashboard(
|
||||
p_cluster text DEFAULT NULL,
|
||||
p_status text DEFAULT NULL,
|
||||
p_window text DEFAULT 'today',
|
||||
p_from timestamptz DEFAULT NULL,
|
||||
p_to timestamptz DEFAULT NULL
|
||||
)
|
||||
RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$
|
||||
DECLARE
|
||||
v_now_eat timestamp;
|
||||
v_from timestamptz;
|
||||
v_to timestamptz;
|
||||
v_preset text;
|
||||
v_days numeric;
|
||||
v_result jsonb;
|
||||
BEGIN
|
||||
p_cluster := NULLIF(p_cluster, '');
|
||||
p_status := NULLIF(p_status, '');
|
||||
v_now_eat := now() AT TIME ZONE 'Africa/Nairobi';
|
||||
|
||||
-- ── resolve the window ──────────────────────────────────────────────────────
|
||||
IF p_from IS NOT NULL OR p_to IS NOT NULL THEN
|
||||
v_preset := 'custom';
|
||||
v_from := COALESCE(p_from, '-infinity'::timestamptz);
|
||||
v_to := COALESCE(p_to, 'infinity'::timestamptz);
|
||||
ELSE
|
||||
v_preset := lower(COALESCE(NULLIF(p_window, ''), 'today'));
|
||||
IF v_preset = 'week' THEN
|
||||
v_from := date_trunc('week', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
|
||||
v_to := (date_trunc('week', v_now_eat) + interval '1 week') AT TIME ZONE 'Africa/Nairobi';
|
||||
ELSIF v_preset = 'month' THEN
|
||||
v_from := date_trunc('month', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
|
||||
v_to := (date_trunc('month', v_now_eat) + interval '1 month') AT TIME ZONE 'Africa/Nairobi';
|
||||
ELSE
|
||||
v_preset := 'today';
|
||||
v_from := date_trunc('day', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
|
||||
v_to := (date_trunc('day', v_now_eat) + interval '1 day') AT TIME ZONE 'Africa/Nairobi';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF v_from > '-infinity'::timestamptz AND v_to < 'infinity'::timestamptz THEN
|
||||
v_days := GREATEST(EXTRACT(EPOCH FROM (v_to - v_from)) / 86400.0, 1);
|
||||
ELSE
|
||||
v_days := NULL; -- open-ended custom window → per-day average not meaningful
|
||||
END IF;
|
||||
|
||||
-- ── build payload ───────────────────────────────────────────────────────────
|
||||
WITH open_t AS (
|
||||
SELECT * FROM tickets.crq_open_sla
|
||||
WHERE (p_cluster IS NULL OR cluster = p_cluster)
|
||||
AND (p_status IS NULL OR normalized_status = p_status)
|
||||
),
|
||||
closed_t AS (
|
||||
SELECT ticket_id, normalized_status, cluster, region, location_name,
|
||||
assigned_team, owner, closed_at, mttr, sla_status, geo_source, geom
|
||||
FROM tickets.crq
|
||||
WHERE NOT COALESCE(is_actionable, false)
|
||||
AND closed_at IS NOT NULL
|
||||
AND closed_at >= v_from AND closed_at < v_to
|
||||
AND (p_cluster IS NULL OR cluster = p_cluster)
|
||||
AND (p_status IS NULL OR normalized_status = p_status)
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'window', jsonb_build_object('from', v_from, 'to', v_to, 'preset', v_preset),
|
||||
|
||||
'open', jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', COALESCE((
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'type', 'Feature',
|
||||
'properties', jsonb_build_object(
|
||||
'ticket_id', ticket_id, 'normalized_status', normalized_status,
|
||||
'cluster', cluster, 'region', region, 'location_name', location_name,
|
||||
'assigned_team', assigned_team, 'owner', initcap(lower(NULLIF(owner, ''))),
|
||||
'geo_source', geo_source,
|
||||
'sla_state', sla_state, 'hours_open', hours_open),
|
||||
'geometry', ST_AsGeoJSON(geom)::jsonb))
|
||||
FROM open_t WHERE geom IS NOT NULL), '[]'::jsonb)
|
||||
),
|
||||
|
||||
'closed', jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', COALESCE((
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'type', 'Feature',
|
||||
'properties', jsonb_build_object(
|
||||
'ticket_id', ticket_id, 'normalized_status', normalized_status,
|
||||
'cluster', cluster, 'region', region, 'location_name', location_name,
|
||||
'assigned_team', assigned_team, 'owner', initcap(lower(NULLIF(owner, ''))),
|
||||
'geo_source', geo_source,
|
||||
'closed_at', closed_at, 'mttr', mttr, 'sla_status', sla_status),
|
||||
'geometry', ST_AsGeoJSON(geom)::jsonb))
|
||||
FROM closed_t WHERE geom IS NOT NULL), '[]'::jsonb)
|
||||
),
|
||||
|
||||
'metrics', jsonb_build_object(
|
||||
'open_now', (SELECT count(*) FROM open_t),
|
||||
'closed_in_window', (SELECT count(*) FROM closed_t),
|
||||
'sla', jsonb_build_object(
|
||||
'open', (SELECT jsonb_build_object(
|
||||
'breached', count(*) FILTER (WHERE sla_state = 'breached'),
|
||||
'at_risk', count(*) FILTER (WHERE sla_state = 'at_risk'),
|
||||
'ok', count(*) FILTER (WHERE sla_state = 'ok'),
|
||||
'unknown', count(*) FILTER (WHERE sla_state = 'unknown')) FROM open_t),
|
||||
'closed', (SELECT jsonb_build_object(
|
||||
'compliant', count(*) FILTER (WHERE sla_status = 'Compliant'),
|
||||
'breached', count(*) FILTER (WHERE sla_status = 'Breached')) FROM closed_t)
|
||||
),
|
||||
'by_status', COALESCE((SELECT jsonb_object_agg(s, c) FROM (
|
||||
SELECT COALESCE(normalized_status, '(none)') AS s, count(*) AS c FROM (
|
||||
SELECT normalized_status FROM open_t
|
||||
UNION ALL SELECT normalized_status FROM closed_t) u GROUP BY 1) z), '{}'::jsonb),
|
||||
'by_cluster', COALESCE((SELECT jsonb_object_agg(cl, c) FROM (
|
||||
SELECT COALESCE(cluster, '(none)') AS cl, count(*) AS c FROM (
|
||||
SELECT cluster FROM open_t
|
||||
UNION ALL SELECT cluster FROM closed_t) u GROUP BY 1) z), '{}'::jsonb),
|
||||
-- closures by engineer (CASE-NORMALIZED owner) — leaderboard for "who closed".
|
||||
'by_owner', COALESCE((SELECT jsonb_agg(jsonb_build_object(
|
||||
'owner', o, 'closed', c, 'breached', b, 'avg_mttr_min', a) ORDER BY c DESC, o)
|
||||
FROM (
|
||||
SELECT COALESCE(initcap(lower(NULLIF(owner, ''))), '(unattributed)') AS o,
|
||||
count(*) AS c,
|
||||
count(*) FILTER (WHERE sla_status = 'Breached') AS b,
|
||||
round(avg(mttr) FILTER (WHERE mttr IS NOT NULL), 1) AS a
|
||||
FROM closed_t GROUP BY 1) z), '[]'::jsonb),
|
||||
'closure_rate', jsonb_build_object(
|
||||
'per_day_avg', CASE WHEN v_days IS NULL THEN NULL
|
||||
ELSE round((SELECT count(*) FROM closed_t)::numeric / v_days, 2) END,
|
||||
'series', COALESCE((SELECT jsonb_agg(jsonb_build_object('day', d, 'count', c) ORDER BY d) FROM (
|
||||
SELECT (closed_at AT TIME ZONE 'Africa/Nairobi')::date AS d, count(*) AS c
|
||||
FROM closed_t GROUP BY 1) z), '[]'::jsonb)
|
||||
),
|
||||
'avg_mttr_min', (SELECT round(avg(mttr), 1) FROM closed_t WHERE mttr IS NOT NULL)
|
||||
),
|
||||
|
||||
'freshness', (SELECT jsonb_object_agg(dataset, jsonb_build_object(
|
||||
'export_type', export_type, 'exported_at', exported_at,
|
||||
'records_ingested', records_ingested, 'ingested_at', ingested_at))
|
||||
FROM tickets.import_meta)
|
||||
) INTO v_result;
|
||||
|
||||
RETURN v_result;
|
||||
END $function$;
|
||||
|
||||
-- ── fn_crq_search — mirror of fn_inc_search over tickets.crq ──────────────────
|
||||
CREATE OR REPLACE FUNCTION reporting.fn_crq_search(
|
||||
p_ticket_id text DEFAULT NULL,
|
||||
p_owner text DEFAULT NULL,
|
||||
p_cluster text DEFAULT NULL,
|
||||
p_status text DEFAULT NULL,
|
||||
p_state text DEFAULT 'closed',
|
||||
p_from timestamptz DEFAULT NULL,
|
||||
p_to timestamptz DEFAULT NULL,
|
||||
p_limit integer DEFAULT 500
|
||||
)
|
||||
RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$
|
||||
DECLARE
|
||||
v_state text := lower(COALESCE(NULLIF(p_state, ''), 'closed'));
|
||||
v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 500), 1), 5000);
|
||||
v_result jsonb;
|
||||
BEGIN
|
||||
p_ticket_id := NULLIF(trim(p_ticket_id), '');
|
||||
p_owner := NULLIF(trim(p_owner), '');
|
||||
p_cluster := NULLIF(p_cluster, '');
|
||||
p_status := NULLIF(p_status, '');
|
||||
|
||||
WITH hits AS (
|
||||
SELECT ticket_id, normalized_status, cluster, region, location_name,
|
||||
initcap(lower(NULLIF(owner, ''))) AS owner, assigned_team,
|
||||
sla_status, mttr, closed_at, created_at_service, is_actionable,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_Y(geom) END AS lat,
|
||||
CASE WHEN geom IS NOT NULL THEN ST_X(geom) END AS lng
|
||||
FROM tickets.crq
|
||||
WHERE (p_ticket_id IS NULL OR ticket_id ILIKE '%' || p_ticket_id || '%')
|
||||
AND (p_owner IS NULL OR lower(owner) LIKE '%' || lower(p_owner) || '%')
|
||||
AND (p_cluster IS NULL OR cluster = p_cluster)
|
||||
AND (p_status IS NULL OR normalized_status = p_status)
|
||||
AND CASE v_state
|
||||
WHEN 'open' THEN COALESCE(is_actionable, false)
|
||||
WHEN 'all' THEN COALESCE(is_actionable, false)
|
||||
OR (closed_at IS NOT NULL
|
||||
AND (p_from IS NULL OR closed_at >= p_from)
|
||||
AND (p_to IS NULL OR closed_at < p_to))
|
||||
ELSE NOT COALESCE(is_actionable, false) -- 'closed'
|
||||
AND closed_at IS NOT NULL
|
||||
AND (p_from IS NULL OR closed_at >= p_from)
|
||||
AND (p_to IS NULL OR closed_at < p_to)
|
||||
END
|
||||
),
|
||||
total AS (SELECT count(*) AS n FROM hits),
|
||||
page AS (
|
||||
SELECT * FROM hits
|
||||
ORDER BY closed_at DESC NULLS LAST, created_at_service DESC NULLS LAST
|
||||
LIMIT v_limit
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'count', (SELECT n FROM total),
|
||||
'truncated', (SELECT n FROM total) > v_limit,
|
||||
'limit', v_limit,
|
||||
'state', v_state,
|
||||
'rows', COALESCE((SELECT jsonb_agg(to_jsonb(page)
|
||||
ORDER BY page.closed_at DESC NULLS LAST,
|
||||
page.created_at_service DESC NULLS LAST)
|
||||
FROM page), '[]'::jsonb)
|
||||
) INTO v_result;
|
||||
|
||||
RETURN v_result;
|
||||
END $function$;
|
||||
|
||||
-- ── fn_crq_filter_options — mirror of fn_inc_filter_options over tickets.crq ──
|
||||
CREATE OR REPLACE FUNCTION reporting.fn_crq_filter_options()
|
||||
RETURNS jsonb LANGUAGE sql STABLE AS $function$
|
||||
SELECT jsonb_build_object(
|
||||
'owners', (SELECT COALESCE(jsonb_agg(o ORDER BY o), '[]'::jsonb)
|
||||
FROM (SELECT DISTINCT initcap(lower(NULLIF(owner, ''))) AS o
|
||||
FROM tickets.crq WHERE NULLIF(owner, '') IS NOT NULL) s),
|
||||
'clusters', (SELECT COALESCE(jsonb_agg(c ORDER BY c), '[]'::jsonb)
|
||||
FROM (SELECT DISTINCT cluster AS c
|
||||
FROM tickets.crq WHERE NULLIF(cluster, '') IS NOT NULL) s),
|
||||
'open_ticket_ids', (SELECT COALESCE(jsonb_agg(ticket_id ORDER BY ticket_id), '[]'::jsonb)
|
||||
FROM tickets.crq WHERE COALESCE(is_actionable, false))
|
||||
);
|
||||
$function$;
|
||||
|
||||
-- ── grants (guarded: roles may not exist on a fresh DB) ──────────────────────
|
||||
DO $grants$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
|
||||
GRANT SELECT ON tickets.crq_open_sla TO dashboard_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_dashboard(text, text, text, timestamptz, timestamptz) TO dashboard_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO dashboard_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_filter_options() TO dashboard_ro;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
||||
GRANT SELECT ON tickets.crq_open_sla TO grafana_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_dashboard(text, text, text, timestamptz, timestamptz) TO grafana_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO grafana_ro;
|
||||
GRANT EXECUTE ON FUNCTION reporting.fn_crq_filter_options() TO grafana_ro;
|
||||
END IF;
|
||||
END $grants$;
|
||||
Loading…
Reference in a new issue