From 2bd5a3e057eeb3922f51b3abb765a1091af74299 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Mar 2026 07:12:29 +0100 Subject: [PATCH] Release v2.2.46: mission schema resilience and repair migration --- .gitignore | 1 + RELEASE_NOTES_v2.2.46.md | 19 ++++ VERSION | 2 +- app/dashboard/backend/mission_service.py | 28 ++++++ migrations/143_mission_control_repair.sql | 110 ++++++++++++++++++++++ 5 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 RELEASE_NOTES_v2.2.46.md create mode 100644 migrations/143_mission_control_repair.sql diff --git a/.gitignore b/.gitignore index 4b54c7c..56fb693 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ htmlcov/ .coverage .pytest_cache/ .mypy_cache/ +RELEASE_NOTES_v2.2.38.md diff --git a/RELEASE_NOTES_v2.2.46.md b/RELEASE_NOTES_v2.2.46.md new file mode 100644 index 0000000..e74f5d6 --- /dev/null +++ b/RELEASE_NOTES_v2.2.46.md @@ -0,0 +1,19 @@ +# Release Notes v2.2.46 + +Dato: 4. marts 2026 + +## Fixes og driftssikring +- Mission Control backend tåler nu manglende mission-tabeller uden at crashe requests, og logger tydelige advarsler. +- Tilføjet idempotent reparationsmigration for Mission Control schema (`143_mission_control_repair.sql`) til miljøer med delvist oprettede tabeller. +- Opdateret `.gitignore` med release-note undtagelse fra tidligere drift. + +## Ændrede filer +- `app/dashboard/backend/mission_service.py` +- `migrations/143_mission_control_repair.sql` +- `.gitignore` +- `VERSION` +- `RELEASE_NOTES_v2.2.46.md` + +## Drift +- Deploy: `./updateto.sh v2.2.46` +- Migration (hvis nødvendig): `docker compose exec db psql -U bmc_hub -d bmc_hub -f migrations/143_mission_control_repair.sql` diff --git a/VERSION b/VERSION index c8e7de6..6ec7dd5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.45 +2.2.46 diff --git a/app/dashboard/backend/mission_service.py b/app/dashboard/backend/mission_service.py index 19ca54d..56dcb38 100644 --- a/app/dashboard/backend/mission_service.py +++ b/app/dashboard/backend/mission_service.py @@ -1,10 +1,19 @@ import json +import logging from typing import Any, Dict, Optional from app.core.database import execute_query, execute_query_single +logger = logging.getLogger(__name__) + + class MissionService: + @staticmethod + def _table_exists(table_name: str) -> bool: + row = execute_query_single("SELECT to_regclass(%s) AS table_name", (f"public.{table_name}",)) + return bool(row and row.get("table_name")) + @staticmethod def get_ring_timeout_seconds() -> int: raw = MissionService.get_setting_value("mission_call_ring_timeout_seconds", "180") or "180" @@ -16,6 +25,9 @@ class MissionService: @staticmethod def expire_stale_ringing_calls() -> None: + if not MissionService._table_exists("mission_call_state"): + return + timeout_seconds = MissionService.get_ring_timeout_seconds() execute_query( """ @@ -108,6 +120,10 @@ class MissionService: customer_name: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: + if not MissionService._table_exists("mission_events"): + logger.warning("Mission table missing: mission_events (event skipped)") + return {} + rows = execute_query( """ INSERT INTO mission_events (event_type, severity, title, source, customer_name, payload) @@ -168,6 +184,10 @@ class MissionService: @staticmethod def get_active_calls() -> list[Dict[str, Any]]: + if not MissionService._table_exists("mission_call_state"): + logger.warning("Mission table missing: mission_call_state (active calls unavailable)") + return [] + MissionService.expire_stale_ringing_calls() rows = execute_query( """ @@ -181,6 +201,10 @@ class MissionService: @staticmethod def get_active_alerts() -> list[Dict[str, Any]]: + if not MissionService._table_exists("mission_uptime_alerts"): + logger.warning("Mission table missing: mission_uptime_alerts (active alerts unavailable)") + return [] + rows = execute_query( """ SELECT alert_key, service_name, customer_name, status, is_active, started_at, resolved_at, updated_at @@ -193,6 +217,10 @@ class MissionService: @staticmethod def get_live_feed(limit: int = 20) -> list[Dict[str, Any]]: + if not MissionService._table_exists("mission_events"): + logger.warning("Mission table missing: mission_events (live feed unavailable)") + return [] + rows = execute_query( """ SELECT id, event_type, severity, title, source, customer_name, payload, created_at diff --git a/migrations/143_mission_control_repair.sql b/migrations/143_mission_control_repair.sql new file mode 100644 index 0000000..6804af8 --- /dev/null +++ b/migrations/143_mission_control_repair.sql @@ -0,0 +1,110 @@ +-- Migration 143: Repair Mission Control schema on partially-initialized databases +-- Safe to run multiple times. + +CREATE TABLE IF NOT EXISTS mission_events ( + id BIGSERIAL PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + severity VARCHAR(16) NOT NULL DEFAULT 'info', + title VARCHAR(255) NOT NULL, + source VARCHAR(64), + customer_name VARCHAR(255), + payload JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS event_type VARCHAR(64); +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS severity VARCHAR(16) NOT NULL DEFAULT 'info'; +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS title VARCHAR(255); +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS source VARCHAR(64); +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS customer_name VARCHAR(255); +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS payload JSONB NOT NULL DEFAULT '{}'::jsonb; +ALTER TABLE mission_events ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; + +CREATE INDEX IF NOT EXISTS idx_mission_events_created_at ON mission_events(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_mission_events_event_type ON mission_events(event_type); + +CREATE TABLE IF NOT EXISTS mission_call_state ( + call_id VARCHAR(128) PRIMARY KEY, + queue_name VARCHAR(128), + caller_number VARCHAR(64), + contact_name VARCHAR(255), + company_name VARCHAR(255), + customer_tag VARCHAR(64), + state VARCHAR(32) NOT NULL, + started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + answered_at TIMESTAMP, + ended_at TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_payload JSONB NOT NULL DEFAULT '{}'::jsonb +); + +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS call_id VARCHAR(128); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS queue_name VARCHAR(128); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS caller_number VARCHAR(64); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS contact_name VARCHAR(255); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS company_name VARCHAR(255); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS customer_tag VARCHAR(64); +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS state VARCHAR(32) NOT NULL DEFAULT 'ringing'; +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS answered_at TIMESTAMP; +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS ended_at TIMESTAMP; +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE mission_call_state ADD COLUMN IF NOT EXISTS last_payload JSONB NOT NULL DEFAULT '{}'::jsonb; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'mission_call_state_pkey' + AND conrelid = 'mission_call_state'::regclass + ) THEN + ALTER TABLE mission_call_state ADD PRIMARY KEY (call_id); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_mission_call_state_state ON mission_call_state(state); +CREATE INDEX IF NOT EXISTS idx_mission_call_state_updated_at ON mission_call_state(updated_at DESC); + +CREATE TABLE IF NOT EXISTS mission_uptime_alerts ( + alert_key VARCHAR(255) PRIMARY KEY, + service_name VARCHAR(255) NOT NULL, + customer_name VARCHAR(255), + status VARCHAR(32) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + started_at TIMESTAMP, + resolved_at TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb, + normalized_payload JSONB NOT NULL DEFAULT '{}'::jsonb +); + +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS alert_key VARCHAR(255); +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS service_name VARCHAR(255); +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS customer_name VARCHAR(255); +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS status VARCHAR(32); +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS started_at TIMESTAMP; +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMP; +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb; +ALTER TABLE mission_uptime_alerts ADD COLUMN IF NOT EXISTS normalized_payload JSONB NOT NULL DEFAULT '{}'::jsonb; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'mission_uptime_alerts_pkey' + AND conrelid = 'mission_uptime_alerts'::regclass + ) THEN + ALTER TABLE mission_uptime_alerts ADD PRIMARY KEY (alert_key); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_mission_uptime_active ON mission_uptime_alerts(is_active, updated_at DESC); + +INSERT INTO settings (key, value, category, description, value_type, is_public) +VALUES + ('mission_call_ring_timeout_seconds', '180', 'mission', 'Seconds before stale ringing calls auto-expire', 'integer', true) +ON CONFLICT (key) DO NOTHING;