2026-03-03 22:11:45 +01:00
|
|
|
import json
|
2026-03-04 07:12:29 +01:00
|
|
|
import logging
|
2026-03-03 22:11:45 +01:00
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
|
|
|
|
from app.core.database import execute_query, execute_query_single
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 07:12:29 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
class MissionService:
|
2026-03-25 13:46:03 +01:00
|
|
|
MISSION_CASE_TYPES = {"ticket", "opgave", "ordre", "projekt", "service"}
|
|
|
|
|
|
2026-03-05 08:41:59 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def _safe(label: str, func, default):
|
|
|
|
|
try:
|
|
|
|
|
return func()
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.error("❌ Mission state component failed: %s (%s)", label, exc)
|
|
|
|
|
return default
|
|
|
|
|
|
2026-03-04 07:12:29 +01:00
|
|
|
@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"))
|
|
|
|
|
|
2026-03-04 00:33:12 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def get_ring_timeout_seconds() -> int:
|
|
|
|
|
raw = MissionService.get_setting_value("mission_call_ring_timeout_seconds", "180") or "180"
|
|
|
|
|
try:
|
|
|
|
|
value = int(raw)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
value = 180
|
|
|
|
|
return max(30, min(value, 3600))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def expire_stale_ringing_calls() -> None:
|
2026-03-04 07:12:29 +01:00
|
|
|
if not MissionService._table_exists("mission_call_state"):
|
|
|
|
|
return
|
|
|
|
|
|
2026-03-04 00:33:12 +01:00
|
|
|
timeout_seconds = MissionService.get_ring_timeout_seconds()
|
|
|
|
|
execute_query(
|
|
|
|
|
"""
|
|
|
|
|
UPDATE mission_call_state
|
|
|
|
|
SET state = 'hangup',
|
|
|
|
|
ended_at = COALESCE(ended_at, NOW()),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE state = 'ringing'
|
|
|
|
|
AND started_at < (NOW() - make_interval(secs => %s))
|
|
|
|
|
""",
|
|
|
|
|
(timeout_seconds,),
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def get_setting_value(key: str, default: Optional[str] = None) -> Optional[str]:
|
|
|
|
|
row = execute_query_single("SELECT value FROM settings WHERE key = %s", (key,))
|
|
|
|
|
if not row:
|
|
|
|
|
return default
|
|
|
|
|
value = row.get("value")
|
|
|
|
|
if value is None or value == "":
|
|
|
|
|
return default
|
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def parse_json_setting(key: str, default: Any) -> Any:
|
|
|
|
|
raw = MissionService.get_setting_value(key, None)
|
|
|
|
|
if raw is None:
|
|
|
|
|
return default
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(raw)
|
|
|
|
|
except Exception:
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def build_alert_key(service_name: str, customer_name: Optional[str]) -> str:
|
|
|
|
|
customer_part = (customer_name or "").strip().lower() or "global"
|
|
|
|
|
return f"{service_name.strip().lower()}::{customer_part}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def resolve_contact_context(caller_number: Optional[str]) -> Dict[str, Optional[str]]:
|
|
|
|
|
if not caller_number:
|
|
|
|
|
return {"contact_name": None, "company_name": None, "customer_tag": None}
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
SELECT
|
|
|
|
|
c.id,
|
|
|
|
|
c.first_name,
|
|
|
|
|
c.last_name,
|
|
|
|
|
(
|
|
|
|
|
SELECT cu.name
|
|
|
|
|
FROM contact_companies cc
|
|
|
|
|
JOIN customers cu ON cu.id = cc.customer_id
|
|
|
|
|
WHERE cc.contact_id = c.id
|
|
|
|
|
ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) AS company_name,
|
|
|
|
|
(
|
|
|
|
|
SELECT t.name
|
|
|
|
|
FROM entity_tags et
|
|
|
|
|
JOIN tags t ON t.id = et.tag_id
|
|
|
|
|
WHERE et.entity_type = 'contact'
|
|
|
|
|
AND et.entity_id = c.id
|
|
|
|
|
AND LOWER(t.name) IN ('vip', 'serviceaftale', 'service agreement')
|
|
|
|
|
ORDER BY t.name
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) AS customer_tag
|
|
|
|
|
FROM contacts c
|
|
|
|
|
WHERE RIGHT(regexp_replace(COALESCE(c.phone, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8)
|
|
|
|
|
OR RIGHT(regexp_replace(COALESCE(c.mobile, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8)
|
|
|
|
|
LIMIT 1
|
|
|
|
|
"""
|
|
|
|
|
row = execute_query_single(query, (caller_number, caller_number))
|
|
|
|
|
if not row:
|
|
|
|
|
return {"contact_name": None, "company_name": None, "customer_tag": None}
|
|
|
|
|
|
|
|
|
|
contact_name = f"{(row.get('first_name') or '').strip()} {(row.get('last_name') or '').strip()}".strip() or None
|
|
|
|
|
return {
|
|
|
|
|
"contact_name": contact_name,
|
|
|
|
|
"company_name": row.get("company_name"),
|
|
|
|
|
"customer_tag": row.get("customer_tag"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def insert_event(
|
|
|
|
|
*,
|
|
|
|
|
event_type: str,
|
|
|
|
|
title: str,
|
|
|
|
|
severity: str = "info",
|
|
|
|
|
source: Optional[str] = None,
|
|
|
|
|
customer_name: Optional[str] = None,
|
|
|
|
|
payload: Optional[Dict[str, Any]] = None,
|
|
|
|
|
) -> Dict[str, Any]:
|
2026-03-04 07:12:29 +01:00
|
|
|
if not MissionService._table_exists("mission_events"):
|
|
|
|
|
logger.warning("Mission table missing: mission_events (event skipped)")
|
|
|
|
|
return {}
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO mission_events (event_type, severity, title, source, customer_name, payload)
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
|
|
|
|
|
RETURNING id, event_type, severity, title, source, customer_name, payload, created_at
|
|
|
|
|
""",
|
|
|
|
|
(event_type, severity, title, source, customer_name, json.dumps(payload or {})),
|
|
|
|
|
)
|
|
|
|
|
return rows[0] if rows else {}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_kpis() -> Dict[str, int]:
|
|
|
|
|
query = """
|
|
|
|
|
SELECT
|
|
|
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet') AS open_cases,
|
|
|
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) = 'åben' AND ansvarlig_bruger_id IS NULL) AS new_cases,
|
|
|
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND ansvarlig_bruger_id IS NULL) AS unassigned_cases,
|
|
|
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date = CURRENT_DATE) AS deadlines_today,
|
|
|
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date < CURRENT_DATE) AS overdue_deadlines
|
|
|
|
|
FROM sag_sager
|
|
|
|
|
"""
|
|
|
|
|
row = execute_query_single(query) or {}
|
|
|
|
|
return {
|
|
|
|
|
"open_cases": int(row.get("open_cases") or 0),
|
|
|
|
|
"new_cases": int(row.get("new_cases") or 0),
|
|
|
|
|
"unassigned_cases": int(row.get("unassigned_cases") or 0),
|
|
|
|
|
"deadlines_today": int(row.get("deadlines_today") or 0),
|
|
|
|
|
"overdue_deadlines": int(row.get("overdue_deadlines") or 0),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_employee_deadlines() -> list[Dict[str, Any]]:
|
|
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT
|
|
|
|
|
COALESCE(u.full_name, u.username, 'Ukendt') AS employee_name,
|
|
|
|
|
COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) AS deadlines_today,
|
|
|
|
|
COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) AS overdue_deadlines
|
|
|
|
|
FROM sag_sager s
|
|
|
|
|
LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id
|
|
|
|
|
WHERE s.deleted_at IS NULL
|
|
|
|
|
AND LOWER(s.status) <> 'afsluttet'
|
|
|
|
|
AND s.deadline IS NOT NULL
|
|
|
|
|
GROUP BY COALESCE(u.full_name, u.username, 'Ukendt')
|
|
|
|
|
HAVING COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) > 0
|
|
|
|
|
OR COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) > 0
|
|
|
|
|
ORDER BY overdue_deadlines DESC, deadlines_today DESC, employee_name ASC
|
|
|
|
|
"""
|
|
|
|
|
) or []
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"employee_name": row.get("employee_name"),
|
|
|
|
|
"deadlines_today": int(row.get("deadlines_today") or 0),
|
|
|
|
|
"overdue_deadlines": int(row.get("overdue_deadlines") or 0),
|
|
|
|
|
}
|
|
|
|
|
for row in rows
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_active_calls() -> list[Dict[str, Any]]:
|
2026-03-04 07:12:29 +01:00
|
|
|
if not MissionService._table_exists("mission_call_state"):
|
|
|
|
|
logger.warning("Mission table missing: mission_call_state (active calls unavailable)")
|
|
|
|
|
return []
|
|
|
|
|
|
2026-03-04 00:33:12 +01:00
|
|
|
MissionService.expire_stale_ringing_calls()
|
2026-03-03 22:11:45 +01:00
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT call_id, queue_name, caller_number, contact_name, company_name, customer_tag, state, started_at, answered_at, ended_at, updated_at
|
|
|
|
|
FROM mission_call_state
|
|
|
|
|
WHERE state = 'ringing'
|
|
|
|
|
ORDER BY started_at DESC
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
return rows or []
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_active_alerts() -> list[Dict[str, Any]]:
|
2026-03-04 07:12:29 +01:00
|
|
|
if not MissionService._table_exists("mission_uptime_alerts"):
|
|
|
|
|
logger.warning("Mission table missing: mission_uptime_alerts (active alerts unavailable)")
|
|
|
|
|
return []
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT alert_key, service_name, customer_name, status, is_active, started_at, resolved_at, updated_at
|
|
|
|
|
FROM mission_uptime_alerts
|
|
|
|
|
WHERE is_active = TRUE
|
|
|
|
|
ORDER BY started_at ASC NULLS LAST
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
return rows or []
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_live_feed(limit: int = 20) -> list[Dict[str, Any]]:
|
2026-03-04 07:12:29 +01:00
|
|
|
if not MissionService._table_exists("mission_events"):
|
|
|
|
|
logger.warning("Mission table missing: mission_events (live feed unavailable)")
|
|
|
|
|
return []
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT id, event_type, severity, title, source, customer_name, payload, created_at
|
|
|
|
|
FROM mission_events
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
LIMIT %s
|
|
|
|
|
""",
|
|
|
|
|
(limit,),
|
|
|
|
|
)
|
|
|
|
|
return rows or []
|
|
|
|
|
|
2026-03-25 13:46:03 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def _normalize_case_type(template_key: Optional[str]) -> str:
|
|
|
|
|
key = (template_key or "").strip().lower()
|
|
|
|
|
if not key:
|
|
|
|
|
return "opgave"
|
|
|
|
|
|
|
|
|
|
if "ticket" in key:
|
|
|
|
|
return "ticket"
|
|
|
|
|
if "ordre" in key or "order" in key:
|
|
|
|
|
return "ordre"
|
|
|
|
|
if "projekt" in key or "project" in key or "pipeline" in key:
|
|
|
|
|
return "projekt"
|
|
|
|
|
if "service" in key or "support" in key:
|
|
|
|
|
return "service"
|
|
|
|
|
if "opgave" in key or "task" in key:
|
|
|
|
|
return "opgave"
|
|
|
|
|
return "opgave"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_important_cases(limit: int = 80) -> list[Dict[str, Any]]:
|
|
|
|
|
if not MissionService._table_exists("sag_sager"):
|
|
|
|
|
logger.warning("Mission table missing: sag_sager (important cases unavailable)")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT
|
|
|
|
|
s.id,
|
|
|
|
|
s.titel,
|
|
|
|
|
s.status,
|
|
|
|
|
s.priority,
|
|
|
|
|
s.deadline,
|
|
|
|
|
s.template_key,
|
|
|
|
|
s.created_at,
|
|
|
|
|
COALESCE(c.name, 'Ukendt kunde') AS customer_name
|
|
|
|
|
FROM sag_sager s
|
|
|
|
|
LEFT JOIN customers c ON c.id = s.customer_id
|
|
|
|
|
WHERE s.deleted_at IS NULL
|
|
|
|
|
AND LOWER(COALESCE(s.status, '')) <> 'afsluttet'
|
|
|
|
|
ORDER BY
|
|
|
|
|
CASE LOWER(COALESCE(s.priority, ''))
|
|
|
|
|
WHEN 'kritisk' THEN 5
|
|
|
|
|
WHEN 'critical' THEN 5
|
|
|
|
|
WHEN 'høj' THEN 4
|
|
|
|
|
WHEN 'hoj' THEN 4
|
|
|
|
|
WHEN 'high' THEN 4
|
|
|
|
|
WHEN 'medium' THEN 3
|
|
|
|
|
WHEN 'normal' THEN 2
|
|
|
|
|
WHEN 'lav' THEN 1
|
|
|
|
|
WHEN 'low' THEN 1
|
|
|
|
|
ELSE 2
|
|
|
|
|
END DESC,
|
|
|
|
|
s.deadline ASC NULLS LAST,
|
|
|
|
|
s.created_at DESC
|
|
|
|
|
LIMIT %s
|
|
|
|
|
""",
|
|
|
|
|
(limit,),
|
|
|
|
|
) or []
|
|
|
|
|
|
|
|
|
|
result: list[Dict[str, Any]] = []
|
|
|
|
|
for row in rows:
|
|
|
|
|
item = dict(row)
|
|
|
|
|
normalized_type = MissionService._normalize_case_type(item.get("template_key"))
|
|
|
|
|
if normalized_type not in MissionService.MISSION_CASE_TYPES:
|
|
|
|
|
normalized_type = "opgave"
|
|
|
|
|
item["case_type"] = normalized_type
|
|
|
|
|
result.append(item)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_recent_emails(limit: int = 25) -> list[Dict[str, Any]]:
|
|
|
|
|
if not MissionService._table_exists("email_messages"):
|
|
|
|
|
logger.warning("Mission table missing: email_messages (recent emails unavailable)")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
rows = execute_query(
|
|
|
|
|
"""
|
|
|
|
|
SELECT
|
|
|
|
|
id,
|
|
|
|
|
subject,
|
|
|
|
|
sender_name,
|
|
|
|
|
sender_email,
|
|
|
|
|
classification,
|
|
|
|
|
status,
|
|
|
|
|
linked_case_id,
|
|
|
|
|
received_date
|
|
|
|
|
FROM email_messages
|
|
|
|
|
WHERE deleted_at IS NULL
|
|
|
|
|
ORDER BY received_date DESC NULLS LAST, created_at DESC
|
|
|
|
|
LIMIT %s
|
|
|
|
|
""",
|
|
|
|
|
(limit,),
|
|
|
|
|
) or []
|
|
|
|
|
|
|
|
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_environment_readings(limit: int = 12) -> list[Dict[str, Any]]:
|
|
|
|
|
readings = MissionService.parse_json_setting("mission_environment_readings", [])
|
|
|
|
|
if not isinstance(readings, list):
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
result: list[Dict[str, Any]] = []
|
|
|
|
|
for raw in readings:
|
|
|
|
|
if not isinstance(raw, dict):
|
|
|
|
|
continue
|
|
|
|
|
item = {
|
|
|
|
|
"sensor_id": str(raw.get("sensor_id") or "").strip() or None,
|
|
|
|
|
"sensor_name": str(raw.get("sensor_name") or "").strip() or "Sensor",
|
|
|
|
|
"temperature": raw.get("temperature"),
|
|
|
|
|
"unit": str(raw.get("unit") or "°C").strip() or "°C",
|
|
|
|
|
"timestamp": raw.get("timestamp"),
|
|
|
|
|
"payload": raw.get("payload") if isinstance(raw.get("payload"), dict) else {},
|
|
|
|
|
}
|
|
|
|
|
result.append(item)
|
|
|
|
|
|
|
|
|
|
return result[: max(1, int(limit))]
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def get_state() -> Dict[str, Any]:
|
2026-03-05 08:41:59 +01:00
|
|
|
kpis_default = {
|
|
|
|
|
"open_cases": 0,
|
|
|
|
|
"new_cases": 0,
|
|
|
|
|
"unassigned_cases": 0,
|
|
|
|
|
"deadlines_today": 0,
|
|
|
|
|
"overdue_deadlines": 0,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 22:11:45 +01:00
|
|
|
return {
|
2026-03-05 08:41:59 +01:00
|
|
|
"kpis": MissionService._safe("kpis", MissionService.get_kpis, kpis_default),
|
|
|
|
|
"active_calls": MissionService._safe("active_calls", MissionService.get_active_calls, []),
|
|
|
|
|
"employee_deadlines": MissionService._safe("employee_deadlines", MissionService.get_employee_deadlines, []),
|
|
|
|
|
"active_alerts": MissionService._safe("active_alerts", MissionService.get_active_alerts, []),
|
|
|
|
|
"live_feed": MissionService._safe("live_feed", lambda: MissionService.get_live_feed(20), []),
|
2026-03-25 13:46:03 +01:00
|
|
|
"important_cases": MissionService._safe("important_cases", lambda: MissionService.get_important_cases(80), []),
|
|
|
|
|
"recent_emails": MissionService._safe("recent_emails", lambda: MissionService.get_recent_emails(25), []),
|
|
|
|
|
"environment_readings": MissionService._safe("environment_readings", lambda: MissionService.get_environment_readings(12), []),
|
2026-03-03 22:11:45 +01:00
|
|
|
"config": {
|
2026-03-05 08:41:59 +01:00
|
|
|
"display_queues": MissionService._safe("config.display_queues", lambda: MissionService.parse_json_setting("mission_display_queues", []), []),
|
|
|
|
|
"sound_enabled": MissionService._safe(
|
|
|
|
|
"config.sound_enabled",
|
|
|
|
|
lambda: str(MissionService.get_setting_value("mission_sound_enabled", "true")).lower() == "true",
|
|
|
|
|
True,
|
|
|
|
|
),
|
|
|
|
|
"sound_volume": MissionService._safe(
|
|
|
|
|
"config.sound_volume",
|
|
|
|
|
lambda: int(MissionService.get_setting_value("mission_sound_volume", "70") or 70),
|
|
|
|
|
70,
|
|
|
|
|
),
|
|
|
|
|
"sound_events": MissionService._safe(
|
|
|
|
|
"config.sound_events",
|
|
|
|
|
lambda: MissionService.parse_json_setting("mission_sound_events", ["incoming_call", "uptime_down", "critical_event"]),
|
|
|
|
|
["incoming_call", "uptime_down", "critical_event"],
|
|
|
|
|
),
|
|
|
|
|
"kpi_visible": MissionService._safe(
|
|
|
|
|
"config.kpi_visible",
|
|
|
|
|
lambda: MissionService.parse_json_setting(
|
|
|
|
|
"mission_kpi_visible",
|
|
|
|
|
["open_cases", "new_cases", "unassigned_cases", "deadlines_today", "overdue_deadlines"],
|
|
|
|
|
),
|
2026-03-03 22:11:45 +01:00
|
|
|
["open_cases", "new_cases", "unassigned_cases", "deadlines_today", "overdue_deadlines"],
|
|
|
|
|
),
|
2026-03-05 08:41:59 +01:00
|
|
|
"customer_filter": MissionService._safe(
|
|
|
|
|
"config.customer_filter",
|
|
|
|
|
lambda: MissionService.get_setting_value("mission_customer_filter", "") or "",
|
|
|
|
|
"",
|
|
|
|
|
),
|
2026-03-25 13:46:03 +01:00
|
|
|
"camera_enabled": MissionService._safe(
|
|
|
|
|
"config.camera_enabled",
|
|
|
|
|
lambda: str(MissionService.get_setting_value("mission_camera_enabled", "false")).lower() == "true",
|
|
|
|
|
False,
|
|
|
|
|
),
|
|
|
|
|
"camera_name": MissionService._safe(
|
|
|
|
|
"config.camera_name",
|
|
|
|
|
lambda: MissionService.get_setting_value("mission_camera_name", "Mission Kamera") or "Mission Kamera",
|
|
|
|
|
"Mission Kamera",
|
|
|
|
|
),
|
|
|
|
|
"camera_feed_url": MissionService._safe(
|
|
|
|
|
"config.camera_feed_url",
|
|
|
|
|
lambda: MissionService.get_setting_value("mission_camera_feed_url", "") or "",
|
|
|
|
|
"",
|
|
|
|
|
),
|
|
|
|
|
"camera_spotlight_seconds": MissionService._safe(
|
|
|
|
|
"config.camera_spotlight_seconds",
|
|
|
|
|
lambda: max(5, min(int(MissionService.get_setting_value("mission_camera_spotlight_seconds", "20") or 20), 120)),
|
|
|
|
|
20,
|
|
|
|
|
),
|
2026-03-03 22:11:45 +01:00
|
|
|
},
|
|
|
|
|
}
|