fix logger import
This commit is contained in:
@@ -21,6 +21,7 @@ from ...core.logger import logger, belief_scope
|
|||||||
from ...core.superset_client import SupersetClient
|
from ...core.superset_client import SupersetClient
|
||||||
from ...services.llm_prompt_templates import normalize_llm_settings
|
from ...services.llm_prompt_templates import normalize_llm_settings
|
||||||
from ...models.llm import ValidationPolicy
|
from ...models.llm import ValidationPolicy
|
||||||
|
from ...models.config import AppConfigRecord
|
||||||
from ...schemas.settings import ValidationPolicyCreate, ValidationPolicyUpdate, ValidationPolicyResponse
|
from ...schemas.settings import ValidationPolicyCreate, ValidationPolicyUpdate, ValidationPolicyResponse
|
||||||
from ...core.database import get_db
|
from ...core.database import get_db
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -345,6 +346,7 @@ async def get_consolidated_settings(
|
|||||||
from ...services.llm_provider import LLMProviderService
|
from ...services.llm_provider import LLMProviderService
|
||||||
from ...core.database import SessionLocal
|
from ...core.database import SessionLocal
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
notifications_payload = {}
|
||||||
try:
|
try:
|
||||||
llm_service = LLMProviderService(db)
|
llm_service = LLMProviderService(db)
|
||||||
providers = llm_service.get_all_providers()
|
providers = llm_service.get_all_providers()
|
||||||
@@ -359,6 +361,10 @@ async def get_consolidated_settings(
|
|||||||
"is_active": p.is_active
|
"is_active": p.is_active
|
||||||
} for p in providers
|
} for p in providers
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config_record = db.query(AppConfigRecord).filter(AppConfigRecord.id == "global").first()
|
||||||
|
if config_record and isinstance(config_record.payload, dict):
|
||||||
|
notifications_payload = config_record.payload.get("notifications", {}) or {}
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -371,7 +377,7 @@ async def get_consolidated_settings(
|
|||||||
llm_providers=llm_providers_list,
|
llm_providers=llm_providers_list,
|
||||||
logging=config.settings.logging.dict(),
|
logging=config.settings.logging.dict(),
|
||||||
storage=config.settings.storage.dict(),
|
storage=config.settings.storage.dict(),
|
||||||
notifications=config.payload.get("notifications", {})
|
notifications=notifications_payload
|
||||||
)
|
)
|
||||||
# [/DEF:get_consolidated_settings:Function]
|
# [/DEF:get_consolidated_settings:Function]
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from ...models.auth import Permission, Role, User
|
from ...models.auth import Permission, Role, User
|
||||||
from ...models.profile import UserDashboardPreference
|
from ...models.profile import UserDashboardPreference
|
||||||
from ..logger import belief_scope
|
from ..logger import belief_scope, logger
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
# [DEF:AuthRepository:Class]
|
# [DEF:AuthRepository:Class]
|
||||||
|
|||||||
@@ -11,21 +11,36 @@
|
|||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
/** @type {{ policy: any, environments: any[], onSave: (p: any) => void, onCancel: () => void }} */
|
/** @type {{ policy: any, environments: any[], onSave: (p: any) => void, onCancel: () => void }} */
|
||||||
let { policy = {}, environments = [], onSave, onCancel } = $props();
|
let { policy = null, environments = [], onSave, onCancel } = $props();
|
||||||
|
|
||||||
let formData = $state({
|
function getSafePolicy() {
|
||||||
name: policy.name || '',
|
return policy && typeof policy === 'object' ? policy : {};
|
||||||
environment_id: policy.environment_id || (environments[0]?.id || ''),
|
}
|
||||||
dashboard_ids: policy.dashboard_ids || [],
|
|
||||||
schedule_days: policy.schedule_days || [1, 2, 3, 4, 5], // Default Mon-Fri
|
|
||||||
window_start: policy.window_start || '01:00',
|
|
||||||
window_end: policy.window_end || '05:00',
|
|
||||||
notify_owners: policy.notify_owners ?? true,
|
|
||||||
alert_condition: policy.alert_condition || 'FAIL_ONLY'
|
|
||||||
});
|
|
||||||
|
|
||||||
|
function getSafeEnvironments() {
|
||||||
|
return Array.isArray(environments) ? environments : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFormData(currentPolicy, currentEnvironments) {
|
||||||
|
return {
|
||||||
|
name: currentPolicy.name || '',
|
||||||
|
environment_id: currentPolicy.environment_id || (currentEnvironments[0]?.id || ''),
|
||||||
|
dashboard_ids: Array.isArray(currentPolicy.dashboard_ids) ? currentPolicy.dashboard_ids : [],
|
||||||
|
schedule_days: Array.isArray(currentPolicy.schedule_days) ? currentPolicy.schedule_days : [1, 2, 3, 4, 5], // Default Mon-Fri
|
||||||
|
window_start: currentPolicy.window_start || '01:00',
|
||||||
|
window_end: currentPolicy.window_end || '05:00',
|
||||||
|
notify_owners: currentPolicy.notify_owners ?? true,
|
||||||
|
alert_condition: currentPolicy.alert_condition || 'FAIL_ONLY'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = $state(buildFormData({}, []));
|
||||||
let isSubmitting = $state(false);
|
let isSubmitting = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
formData = buildFormData(getSafePolicy(), getSafeEnvironments());
|
||||||
|
});
|
||||||
|
|
||||||
const days = [
|
const days = [
|
||||||
{ id: 0, label: 'Sun' },
|
{ id: 0, label: 'Sun' },
|
||||||
{ id: 1, label: 'Mon' },
|
{ id: 1, label: 'Mon' },
|
||||||
@@ -88,14 +103,14 @@
|
|||||||
<select id="env" bind:value={formData.environment_id}
|
<select id="env" bind:value={formData.environment_id}
|
||||||
class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
disabled={isSubmitting}>
|
disabled={isSubmitting}>
|
||||||
{#each environments as env}
|
{#each getSafeEnvironments() as env}
|
||||||
<option value={env.id}>{env.name || env.id}</option>
|
<option value={env.id}>{env.name || env.id}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Schedule Days</label>
|
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300">Schedule Days</span>
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
{#each days as day}
|
{#each days as day}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|||||||
@@ -36,6 +36,47 @@
|
|||||||
getValidationPolicies(),
|
getValidationPolicies(),
|
||||||
getEnvironments()
|
getEnvironments()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const policyArray = Array.isArray(policiesData) ? policiesData : [];
|
||||||
|
const environmentArray = Array.isArray(envsData) ? envsData : [];
|
||||||
|
|
||||||
|
const invalidPolicies = policyArray
|
||||||
|
.map((policy, index) => ({ index, policy }))
|
||||||
|
.filter(({ policy }) => !policy || typeof policy !== 'object');
|
||||||
|
const policiesWithMissingName = policyArray
|
||||||
|
.map((policy, index) => ({ index, policy }))
|
||||||
|
.filter(({ policy }) => policy && (policy.name === null || policy.name === undefined));
|
||||||
|
|
||||||
|
const invalidEnvironments = environmentArray
|
||||||
|
.map((env, index) => ({ index, env }))
|
||||||
|
.filter(({ env }) => !env || typeof env !== 'object');
|
||||||
|
const environmentsWithMissingName = environmentArray
|
||||||
|
.map((env, index) => ({ index, env }))
|
||||||
|
.filter(({ env }) => env && (env.name === null || env.name === undefined));
|
||||||
|
|
||||||
|
console.log('[AutomationSettingsPage][Debug] Loaded payload shapes', {
|
||||||
|
policiesCount: policyArray.length,
|
||||||
|
environmentsCount: environmentArray.length,
|
||||||
|
invalidPoliciesCount: invalidPolicies.length,
|
||||||
|
policiesWithMissingNameCount: policiesWithMissingName.length,
|
||||||
|
invalidEnvironmentsCount: invalidEnvironments.length,
|
||||||
|
environmentsWithMissingNameCount: environmentsWithMissingName.length
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invalidPolicies.length > 0 || policiesWithMissingName.length > 0) {
|
||||||
|
console.warn('[AutomationSettingsPage][Debug] Suspicious policy payload detected', {
|
||||||
|
invalidPolicies,
|
||||||
|
policiesWithMissingName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidEnvironments.length > 0 || environmentsWithMissingName.length > 0) {
|
||||||
|
console.warn('[AutomationSettingsPage][Debug] Suspicious environments payload detected', {
|
||||||
|
invalidEnvironments,
|
||||||
|
environmentsWithMissingName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
policies = policiesData;
|
policies = policiesData;
|
||||||
environments = envsData;
|
environments = envsData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,6 +92,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(policy) {
|
function handleEdit(policy) {
|
||||||
|
if (!policy) {
|
||||||
|
console.error('[AutomationSettingsPage][Debug] handleEdit received invalid policy', { policy });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[AutomationSettingsPage][Debug] handleEdit policy snapshot', {
|
||||||
|
id: policy.id,
|
||||||
|
name: policy.name,
|
||||||
|
environment_id: policy.environment_id,
|
||||||
|
dashboard_ids_type: Array.isArray(policy.dashboard_ids) ? 'array' : typeof policy.dashboard_ids
|
||||||
|
});
|
||||||
selectedPolicy = policy;
|
selectedPolicy = policy;
|
||||||
showForm = true;
|
showForm = true;
|
||||||
}
|
}
|
||||||
@@ -83,10 +134,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEnvName(id) {
|
function getEnvName(id) {
|
||||||
return environments.find(e => e.id === id)?.name || id;
|
const envMatch = environments.find((e) => e?.id === id);
|
||||||
|
if (!envMatch) {
|
||||||
|
console.warn('[AutomationSettingsPage][Debug] Environment not found for policy environment_id', {
|
||||||
|
requestedEnvironmentId: id,
|
||||||
|
environmentsCount: Array.isArray(environments) ? environments.length : -1
|
||||||
|
});
|
||||||
|
} else if (envMatch.name === null || envMatch.name === undefined) {
|
||||||
|
console.warn('[AutomationSettingsPage][Debug] Environment has null/undefined name', {
|
||||||
|
requestedEnvironmentId: id,
|
||||||
|
environment: envMatch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return envMatch?.name || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
const policiesWithNullName = (Array.isArray(policies) ? policies : []).filter(
|
||||||
|
(policy) => policy && (policy.name === null || policy.name === undefined)
|
||||||
|
);
|
||||||
|
const policyEntriesWithNullDashboardIds = (Array.isArray(policies) ? policies : []).filter(
|
||||||
|
(policy) => policy && !Array.isArray(policy.dashboard_ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (policiesWithNullName.length > 0 || policyEntriesWithNullDashboardIds.length > 0) {
|
||||||
|
console.warn('[AutomationSettingsPage][Debug] Render-time suspicious policy data detected', {
|
||||||
|
policiesWithNullName,
|
||||||
|
policyEntriesWithNullDashboardIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6 max-w-5xl">
|
<div class="container mx-auto p-6 max-w-5xl">
|
||||||
|
|||||||
109
init_task.md
109
init_task.md
@@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Это абсолютно логичный следующий шаг. Переход от **Pull-модели** (пользователь сам заходит в `ss-tools` посмотреть, не сломалось ли чего) к **Push-модели** (система сама бьет тревогу) — это стандарт для любого Enterprise инструмента.
|
|
||||||
|
|
||||||
Более того, раз вы **уже внедрили профили пользователей и привязку аккаунтов Superset** (как мы обсуждали в предыдущем шаге), у вас открывается возможность делать **"Умный роутинг" (Smart Routing)** уведомлений.
|
|
||||||
|
|
||||||
Вот как лучше всего спроектировать эту систему (UI, логика и форматы отчетов).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. Умный роутинг (Киллер-фича благодаря профилям)
|
|
||||||
|
|
||||||
Вам не обязательно заставлять пользователей вручную вбивать email-ы для каждого дашборда.
|
|
||||||
В Superset у каждого дашборда есть `owners` (владельцы).
|
|
||||||
|
|
||||||
**Логика:**
|
|
||||||
1. LLM-проверка дашборда завершается со статусом `FAIL`.
|
|
||||||
2. Бэкенд `ss-tools` смотрит: кто `owners` у этого дашборда в Superset? (например, `admin` и `ivan.ivanov`).
|
|
||||||
3. Бэкенд ищет этих пользователей в базе профилей `ss-tools`.
|
|
||||||
4. Если у `ivan.ivanov` в профиле `ss-tools` прописан Email или Telegram ID, система **автоматически** отправляет ему персональное уведомление: *"Привет! Твой дашборд упал"*.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Как встроить это в UI (Настройки)
|
|
||||||
|
|
||||||
Разделите настройки на **Глобальные провайдеры** (для админов) и **Правила отправки** (для пользователей/задач).
|
|
||||||
|
|
||||||
#### А. Настройки интеграций (Global Settings -> Notifications)
|
|
||||||
Здесь администратор системы один раз настраивает доступы.
|
|
||||||
* **Email (SMTP):** Хост, Порт, Логин, Пароль, От кого (noreply@ss-tools.com).
|
|
||||||
* **Корпоративный мессенджер:** Webhook URL или Bot Token.
|
|
||||||
* **Telegram:** Bot Token.
|
|
||||||
|
|
||||||
#### Б. Настройка правил в "Политике проверки" (Validation Policy)
|
|
||||||
Когда пользователь настраивает расписание проверок (о котором мы говорили ранее), добавьте секцию "Уведомления".
|
|
||||||
|
|
||||||
```text
|
|
||||||
[ Настройка LLM-проверки: Nightly Prod Check ]
|
|
||||||
|
|
||||||
... (выбор дашбордов и расписания) ...
|
|
||||||
|
|
||||||
🔔 Уведомления об инцидентах:
|
|
||||||
Отправлять при статусе:[ Только FAIL ⌄ ] (Опции: FAIL, FAIL + WARN, Всегда)
|
|
||||||
|
|
||||||
Куда отправлять:
|
|
||||||
[x] Автоматически Владельцам (Owners) дашборда (Email / Мессенджер из их профиля)
|
|
||||||
[x] В общий канал / группу:
|
|
||||||
+ Добавить канал
|
|
||||||
[ Slack ⌄ ] -> Канал: [ #bi-alerts ]
|
|
||||||
[ Email ⌄ ] -> Адрес:[ data-team@company.com ]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Дизайн самих уведомлений (Payload)
|
|
||||||
|
|
||||||
Очень важно, чтобы уведомление не было простыней логов, иначе его начнут игнорировать (Alert Fatigue).
|
|
||||||
|
|
||||||
#### ✉️ Формат Email-письма (Богатый формат)
|
|
||||||
Письмо должно содержать всё необходимое, чтобы понять проблему без перехода в систему.
|
|
||||||
|
|
||||||
* **Тема:** 🔴 [ss-tools] Ошибка валидации: Sales Executive Dashboard
|
|
||||||
* **Тело (HTML):**
|
|
||||||
* **Заголовок:** 🔴 **FAIL:** Sales Executive Dashboard (Окружение: PROD)
|
|
||||||
* **Краткий вердикт LLM:** "Сломан чарт Revenue YTD. Ошибка базы данных: колонка profit не найдена."
|
|
||||||
* **Картинка:** Вшитый скриншот дашборда с обведенной ошибкой (как он генерируется в вашей системе).
|
|
||||||
* **Кнопка:** `[ Открыть полный отчет в ss-tools ]` (Deep link прямо на страницу с логами).
|
|
||||||
|
|
||||||
#### 💬 Формат для Мессенджеров (Slack / Telegram)
|
|
||||||
В мессенджере картинки и длинные тексты читаются хуже. Формат должен быть сверх-компактным.
|
|
||||||
|
|
||||||
**Сообщение от бота:**
|
|
||||||
> 🚨 **Dashboard Validation Failed**
|
|
||||||
> **Dashboard:** [Sales Executive](link-to-ss-tools-report)
|
|
||||||
> **Env:** ss-prod | **Time:** 03:15 AM
|
|
||||||
>
|
|
||||||
> 🤖 **LLM Summary:**
|
|
||||||
> Обнаружена SQL-ошибка в чарте "Revenue". Данные не отображаются.
|
|
||||||
>
|
|
||||||
> 🔗 `[View in ss-tools]` 📊 `[Open in Superset]`
|
|
||||||
|
|
||||||
*(Для Slack/Telegram можно прикрепить скриншот вторым сообщением или вложением, если API позволяет, но текст должен быть первичным).*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Архитектура Бэкенда (Как это реализовать в FastAPI)
|
|
||||||
|
|
||||||
Чтобы код не превратился в спагетти, используйте паттерн "Издатель-Подписчик" (Pub/Sub) или абстрактную фабрику уведомлений.
|
|
||||||
|
|
||||||
1. **Создайте базовый класс `NotificationProvider`**:
|
|
||||||
```python
|
|
||||||
class NotificationProvider(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
async def send(self, report: ValidationResult, recipient: str):
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
2. **Реализуйте провайдеры**: `SmtpEmailProvider`, `SlackWebhookProvider`, `TelegramBotProvider`.
|
|
||||||
3. **Слой `NotificationService`**:
|
|
||||||
В конце выполнения `DashboardValidationPlugin.execute()`, плагин вызывает:
|
|
||||||
```python
|
|
||||||
await notification_service.dispatch_report(validation_result)
|
|
||||||
```
|
|
||||||
4. Служба `NotificationService` смотрит в БД настройки расписания и профили владельцев, решает, *кому* отправлять, и вызывает нужные провайдеры.
|
|
||||||
5. **Фоновые задачи:** Сама отправка почты/сообщений не должна тормозить основной поток задачи. Оберните вызов в `BackgroundTasks` FastAPI или отправляйте через Celery/Redis Queue, чтобы таска завершилась мгновенно, а письмо ушло асинхронно.
|
|
||||||
|
|
||||||
### Итог по UX:
|
|
||||||
Пользователь заходит в свой профиль -> вводит свой Telegram ID -> ставит галочку "Уведомлять меня о моих упавших дашбордах".
|
|
||||||
Всё! Дальше система сама берет на себя всю рутину. Если ночью дашборд, где он Owner, упал — он утром получает сообщение в телегу со скриншотом. Это "Wow-эффект" для конечного пользователя.
|
|
||||||
@@ -573,3 +573,7 @@ All implementation tasks MUST follow the Design-by-Contract specifications:
|
|||||||
|
|
||||||
- [x] D095 [US3] Migrate deprecated native event directives to event attributes in [`+page.svelte`](frontend/src/routes/dashboards/+page.svelte), fix modal propagation handlers, and address targeted a11y warnings in [`automation/+page.svelte`](frontend/src/routes/settings/automation/+page.svelte).
|
- [x] D095 [US3] Migrate deprecated native event directives to event attributes in [`+page.svelte`](frontend/src/routes/dashboards/+page.svelte), fix modal propagation handlers, and address targeted a11y warnings in [`automation/+page.svelte`](frontend/src/routes/settings/automation/+page.svelte).
|
||||||
- [x] D096 [US6] Resolve blocking parse error in [`ProtectedRoute.svelte`](frontend/src/components/auth/ProtectedRoute.svelte) by removing HTML comments from script scope and aligning rendering with Svelte 5 (`{@render ...}`).
|
- [x] D096 [US6] Resolve blocking parse error in [`ProtectedRoute.svelte`](frontend/src/components/auth/ProtectedRoute.svelte) by removing HTML comments from script scope and aligning rendering with Svelte 5 (`{@render ...}`).
|
||||||
|
- [x] D097 [US1] Fix auth backend runtime failure by importing logger in [`AuthRepository`](backend/src/core/auth/repository.py) to eliminate `NameError` during [`POST /api/auth/login`](backend/src/api/auth.py).
|
||||||
|
- [x] D098 [US6] Fix consolidated settings 500 by replacing invalid `config.payload` access in [`settings router`](backend/src/api/routes/settings.py) with DB-backed `AppConfigRecord` notifications extraction and safe fallback.
|
||||||
|
- [x] D099 [US6] Diagnose Settings→Automation click crash for Svelte 5: confirmed runes-mode compile blocker in [`automation/+page.svelte`](frontend/src/routes/settings/automation/+page.svelte) (`$:` not allowed) and validated likely runtime null-source in [`PolicyForm.svelte`](frontend/src/lib/components/health/PolicyForm.svelte) where `policy={null}` can trigger `policy.name` access during add-flow initialization.
|
||||||
|
- [x] D100 [US6] Fix Settings→Automation policy form null crash for Svelte 5 by making [`PolicyForm.svelte`](frontend/src/lib/components/health/PolicyForm.svelte) null-safe for `policy`/`environments` and removing runes `state_referenced_locally` warning pattern from form initialization.
|
||||||
|
|||||||
Reference in New Issue
Block a user