운영 지표와 지원 요청 저장소 추가
This commit is contained in:
parent
50fa4ad432
commit
e7ebb38177
20 changed files with 3038 additions and 39 deletions
|
|
@ -343,15 +343,46 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
WHERE stage_code IN ('라포','탐색','개입','정리')
|
||||
) AS has_stage_defs,
|
||||
to_regclass('app.admin_health_event') IS NOT NULL AS has_admin_health_event,
|
||||
to_regclass('app.admin_health_daily_rollup') IS NOT NULL AS has_admin_health_daily_rollup,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'app'
|
||||
AND table_name = 'admin_health_daily_rollup'
|
||||
AND column_name = 'last_down_at'
|
||||
) AS has_admin_health_daily_rollup_columns,
|
||||
to_regclass('app.support_ticket') IS NOT NULL AS has_support_ticket,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'app'
|
||||
AND table_name = 'support_ticket'
|
||||
AND column_name IN ('fingerprint', 'parent_ticket_id')
|
||||
GROUP BY table_schema, table_name
|
||||
HAVING count(*) = 2
|
||||
) AS has_support_ticket_duplicate_columns,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'app'
|
||||
AND tablename = 'admin_health_event'
|
||||
AND policyname IN ('p_admin_health_event_select','p_admin_health_event_insert')
|
||||
AND policyname IN (
|
||||
'p_admin_health_event_select',
|
||||
'p_admin_health_event_insert',
|
||||
'p_admin_health_event_delete'
|
||||
)
|
||||
GROUP BY schemaname, tablename
|
||||
HAVING count(*) = 2
|
||||
HAVING count(*) = 3
|
||||
) AS has_admin_health_event_policies,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'app'
|
||||
AND tablename = 'admin_health_daily_rollup'
|
||||
AND policyname IN (
|
||||
'p_admin_health_daily_rollup_select',
|
||||
'p_admin_health_daily_rollup_insert',
|
||||
'p_admin_health_daily_rollup_update'
|
||||
)
|
||||
GROUP BY schemaname, tablename
|
||||
HAVING count(*) = 3
|
||||
) AS has_admin_health_daily_rollup_policies,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'app'
|
||||
|
|
@ -365,6 +396,19 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
GROUP BY schemaname, tablename
|
||||
HAVING count(*) = 4
|
||||
) AS has_support_ticket_policies,
|
||||
to_regclass('app.learner_prepost_measure') IS NOT NULL AS has_learner_prepost_measure,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'app'
|
||||
AND tablename = 'learner_prepost_measure'
|
||||
AND policyname IN (
|
||||
'p_learner_prepost_measure_select',
|
||||
'p_learner_prepost_measure_insert',
|
||||
'p_learner_prepost_measure_update'
|
||||
)
|
||||
GROUP BY schemaname, tablename
|
||||
HAVING count(*) = 3
|
||||
) AS has_learner_prepost_measure_policies,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'app'
|
||||
|
|
@ -407,9 +451,15 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
and row["has_state_columns"]
|
||||
and row["has_stage_defs"]
|
||||
and row["has_admin_health_event"]
|
||||
and row["has_admin_health_daily_rollup"]
|
||||
and row["has_admin_health_daily_rollup_columns"]
|
||||
and row["has_support_ticket"]
|
||||
and row["has_support_ticket_duplicate_columns"]
|
||||
and row["has_admin_health_event_policies"]
|
||||
and row["has_admin_health_daily_rollup_policies"]
|
||||
and row["has_support_ticket_policies"]
|
||||
and row["has_learner_prepost_measure"]
|
||||
and row["has_learner_prepost_measure_policies"]
|
||||
and row["has_session_write_policies"]
|
||||
and row["removed_old_session_policy"]
|
||||
and row["has_turn_write_policies"]
|
||||
|
|
@ -574,6 +624,34 @@ async def ensure_runtime_tables() -> None:
|
|||
ON app.admin_health_event(observed_at DESC, service_key)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app.admin_health_daily_rollup (
|
||||
rollup_date DATE NOT NULL,
|
||||
environment TEXT NOT NULL,
|
||||
engine_mode TEXT NOT NULL,
|
||||
service_key TEXT NOT NULL,
|
||||
service_name TEXT NOT NULL,
|
||||
sample_count INTEGER NOT NULL DEFAULT 0 CHECK (sample_count >= 0),
|
||||
ok_samples INTEGER NOT NULL DEFAULT 0 CHECK (ok_samples >= 0),
|
||||
degraded_samples INTEGER NOT NULL DEFAULT 0 CHECK (degraded_samples >= 0),
|
||||
down_samples INTEGER NOT NULL DEFAULT 0 CHECK (down_samples >= 0),
|
||||
first_observed_at TIMESTAMPTZ NOT NULL,
|
||||
last_observed_at TIMESTAMPTZ NOT NULL,
|
||||
latest_status TEXT NOT NULL CHECK (latest_status IN ('ok','degraded','down')),
|
||||
last_down_at TIMESTAMPTZ,
|
||||
max_load REAL NOT NULL DEFAULT 0.0 CHECK (max_load >= 0.0 AND max_load <= 1.0),
|
||||
generated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (rollup_date, environment, engine_mode, service_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_health_daily_rollup_latest
|
||||
ON app.admin_health_daily_rollup(last_observed_at DESC, service_key)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app.support_ticket (
|
||||
|
|
@ -599,6 +677,8 @@ async def ensure_runtime_tables() -> None:
|
|||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
source_path TEXT NOT NULL DEFAULT '',
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
parent_ticket_id UUID REFERENCES app.support_ticket(id) ON DELETE SET NULL,
|
||||
assigned_group TEXT NOT NULL DEFAULT '',
|
||||
resolution_note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
|
@ -620,17 +700,95 @@ async def ensure_runtime_tables() -> None:
|
|||
ON app.support_ticket(reporter_id, created_at DESC)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.support_ticket
|
||||
ADD COLUMN IF NOT EXISTS fingerprint TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS parent_ticket_id UUID REFERENCES app.support_ticket(id) ON DELETE SET NULL
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_support_ticket_fingerprint
|
||||
ON app.support_ticket(fingerprint, created_at DESC)
|
||||
WHERE fingerprint <> ''
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_support_ticket_parent
|
||||
ON app.support_ticket(parent_ticket_id)
|
||||
WHERE parent_ticket_id IS NOT NULL
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app.learner_prepost_measure (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE CASCADE,
|
||||
pilot_id TEXT NOT NULL DEFAULT 'phase3-pilot-draft',
|
||||
measure_name TEXT NOT NULL CHECK (
|
||||
measure_name IN ('self_efficacy','skill_proficiency','training_satisfaction')
|
||||
),
|
||||
timepoint TEXT NOT NULL CHECK (timepoint IN ('pre','post')),
|
||||
raw_score NUMERIC(8,3) NOT NULL,
|
||||
min_score NUMERIC(8,3) NOT NULL DEFAULT 1.0,
|
||||
max_score NUMERIC(8,3) NOT NULL DEFAULT 5.0,
|
||||
instrument_version TEXT NOT NULL DEFAULT 'pilot-prepost-scaffold-2026-06-28',
|
||||
item_count INTEGER NOT NULL DEFAULT 1 CHECK (item_count >= 1 AND item_count <= 80),
|
||||
collected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (max_score > min_score),
|
||||
CHECK (raw_score >= min_score AND raw_score <= max_score),
|
||||
UNIQUE (learner_id, pilot_id, measure_name, timepoint, instrument_version)
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_learner_prepost_measure_pilot
|
||||
ON app.learner_prepost_measure(pilot_id, measure_name, timepoint)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_learner_prepost_measure_learner
|
||||
ON app.learner_prepost_measure(learner_id, pilot_id)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.admin_health_event ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS p_admin_health_event_select ON app.admin_health_event;
|
||||
DROP POLICY IF EXISTS p_admin_health_event_insert ON app.admin_health_event;
|
||||
DROP POLICY IF EXISTS p_admin_health_event_delete ON app.admin_health_event;
|
||||
CREATE POLICY p_admin_health_event_select
|
||||
ON app.admin_health_event FOR SELECT
|
||||
USING (app.current_role_name() = 'admin');
|
||||
CREATE POLICY p_admin_health_event_insert
|
||||
ON app.admin_health_event FOR INSERT
|
||||
WITH CHECK (app.current_role_name() = 'admin');
|
||||
CREATE POLICY p_admin_health_event_delete
|
||||
ON app.admin_health_event FOR DELETE
|
||||
USING (app.current_role_name() = 'admin');
|
||||
|
||||
ALTER TABLE app.admin_health_daily_rollup ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS p_admin_health_daily_rollup_select ON app.admin_health_daily_rollup;
|
||||
DROP POLICY IF EXISTS p_admin_health_daily_rollup_insert ON app.admin_health_daily_rollup;
|
||||
DROP POLICY IF EXISTS p_admin_health_daily_rollup_update ON app.admin_health_daily_rollup;
|
||||
DROP POLICY IF EXISTS p_admin_health_daily_rollup_delete ON app.admin_health_daily_rollup;
|
||||
ALTER TABLE app.admin_health_daily_rollup
|
||||
ADD COLUMN IF NOT EXISTS last_down_at TIMESTAMPTZ;
|
||||
CREATE POLICY p_admin_health_daily_rollup_select
|
||||
ON app.admin_health_daily_rollup FOR SELECT
|
||||
USING (app.current_role_name() = 'admin');
|
||||
CREATE POLICY p_admin_health_daily_rollup_insert
|
||||
ON app.admin_health_daily_rollup FOR INSERT
|
||||
WITH CHECK (app.current_role_name() = 'admin');
|
||||
CREATE POLICY p_admin_health_daily_rollup_update
|
||||
ON app.admin_health_daily_rollup FOR UPDATE
|
||||
USING (app.current_role_name() = 'admin')
|
||||
WITH CHECK (app.current_role_name() = 'admin');
|
||||
|
||||
ALTER TABLE app.support_ticket ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS p_support_ticket_select ON app.support_ticket;
|
||||
|
|
@ -656,6 +814,41 @@ async def ensure_runtime_tables() -> None:
|
|||
CREATE POLICY p_support_ticket_delete
|
||||
ON app.support_ticket FOR DELETE
|
||||
USING (app.current_role_name() = 'admin');
|
||||
|
||||
ALTER TABLE app.learner_prepost_measure ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS p_learner_prepost_measure_select ON app.learner_prepost_measure;
|
||||
DROP POLICY IF EXISTS p_learner_prepost_measure_insert ON app.learner_prepost_measure;
|
||||
DROP POLICY IF EXISTS p_learner_prepost_measure_update ON app.learner_prepost_measure;
|
||||
CREATE POLICY p_learner_prepost_measure_select
|
||||
ON app.learner_prepost_measure FOR SELECT
|
||||
USING (
|
||||
app.current_role_name() = 'admin'
|
||||
OR learner_id = app.current_uid()
|
||||
OR (
|
||||
app.current_role_name() = 'instructor'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM app.app_user u
|
||||
WHERE u.user_id = app.learner_prepost_measure.learner_id
|
||||
AND u.cohort = current_setting('app.current_cohort', true)
|
||||
)
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_learner_prepost_measure_insert
|
||||
ON app.learner_prepost_measure FOR INSERT
|
||||
WITH CHECK (
|
||||
app.current_role_name() = 'admin'
|
||||
OR learner_id = app.current_uid()
|
||||
);
|
||||
CREATE POLICY p_learner_prepost_measure_update
|
||||
ON app.learner_prepost_measure FOR UPDATE
|
||||
USING (
|
||||
app.current_role_name() = 'admin'
|
||||
OR learner_id = app.current_uid()
|
||||
)
|
||||
WITH CHECK (
|
||||
app.current_role_name() = 'admin'
|
||||
OR learner_id = app.current_uid()
|
||||
);
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
|
|
@ -674,6 +867,12 @@ async def ensure_runtime_tables() -> None:
|
|||
ADD COLUMN IF NOT EXISTS turns_in_stage INT NOT NULL DEFAULT 0
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.turns
|
||||
ADD COLUMN IF NOT EXISTS provider_events JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.stage_def (stage_code, display_name, seq, base_openness)
|
||||
|
|
@ -730,6 +929,37 @@ async def ensure_runtime_tables() -> None:
|
|||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.pinned_fact ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS p_pinned_insert ON app.pinned_fact;
|
||||
DROP POLICY IF EXISTS p_pinned_update ON app.pinned_fact;
|
||||
|
||||
CREATE POLICY p_pinned_insert ON app.pinned_fact FOR INSERT WITH CHECK (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM app.case_profile cp
|
||||
WHERE cp.case_id = app.pinned_fact.case_id
|
||||
AND cp.learner_id = app.current_uid()
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_pinned_update ON app.pinned_fact FOR UPDATE USING (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM app.case_profile cp
|
||||
WHERE cp.case_id = app.pinned_fact.case_id
|
||||
AND cp.learner_id = app.current_uid()
|
||||
)
|
||||
) WITH CHECK (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM app.case_profile cp
|
||||
WHERE cp.case_id = app.pinned_fact.case_id
|
||||
AND cp.learner_id = app.current_uid()
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
DROP POLICY IF EXISTS p_turns_modify ON app.turns;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue