mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
Update notification service
This commit is contained in:
@@ -57,7 +57,7 @@ interface NotificationConfig {
|
|||||||
channel_overrides: Record<string, ChannelOverrides>
|
channel_overrides: Record<string, ChannelOverrides>
|
||||||
ai_enabled: boolean
|
ai_enabled: boolean
|
||||||
ai_provider: string
|
ai_provider: string
|
||||||
ai_api_key: string
|
ai_api_keys: Record<string, string> // Per-provider API keys
|
||||||
ai_model: string
|
ai_model: string
|
||||||
ai_language: string
|
ai_language: string
|
||||||
ai_ollama_url: string
|
ai_ollama_url: string
|
||||||
@@ -208,7 +208,13 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
},
|
},
|
||||||
ai_enabled: false,
|
ai_enabled: false,
|
||||||
ai_provider: "groq",
|
ai_provider: "groq",
|
||||||
ai_api_key: "",
|
ai_api_keys: {
|
||||||
|
groq: "",
|
||||||
|
gemini: "",
|
||||||
|
anthropic: "",
|
||||||
|
openai: "",
|
||||||
|
openrouter: "",
|
||||||
|
},
|
||||||
ai_model: "",
|
ai_model: "",
|
||||||
ai_language: "en",
|
ai_language: "en",
|
||||||
ai_ollama_url: "http://localhost:11434",
|
ai_ollama_url: "http://localhost:11434",
|
||||||
@@ -261,8 +267,19 @@ export function NotificationSettings() {
|
|||||||
const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings")
|
const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings")
|
||||||
if (data.success && data.config) {
|
if (data.success && data.config) {
|
||||||
// Backend automatically migrates deprecated AI models to current versions
|
// Backend automatically migrates deprecated AI models to current versions
|
||||||
setConfig(data.config)
|
// Ensure ai_api_keys object exists (fallback for older configs)
|
||||||
setOriginalConfig(data.config)
|
const configWithKeys = {
|
||||||
|
...data.config,
|
||||||
|
ai_api_keys: data.config.ai_api_keys || {
|
||||||
|
groq: "",
|
||||||
|
gemini: "",
|
||||||
|
anthropic: "",
|
||||||
|
openai: "",
|
||||||
|
openrouter: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConfig(configWithKeys)
|
||||||
|
setOriginalConfig(configWithKeys)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load notification settings:", err)
|
console.error("Failed to load notification settings:", err)
|
||||||
@@ -467,7 +484,6 @@ export function NotificationSettings() {
|
|||||||
enabled: String(cfg.enabled),
|
enabled: String(cfg.enabled),
|
||||||
ai_enabled: String(cfg.ai_enabled),
|
ai_enabled: String(cfg.ai_enabled),
|
||||||
ai_provider: cfg.ai_provider,
|
ai_provider: cfg.ai_provider,
|
||||||
ai_api_key: cfg.ai_api_key,
|
|
||||||
ai_model: cfg.ai_model,
|
ai_model: cfg.ai_model,
|
||||||
ai_language: cfg.ai_language,
|
ai_language: cfg.ai_language,
|
||||||
ai_ollama_url: cfg.ai_ollama_url,
|
ai_ollama_url: cfg.ai_ollama_url,
|
||||||
@@ -479,6 +495,14 @@ export function NotificationSettings() {
|
|||||||
pve_host: cfg.pve_host,
|
pve_host: cfg.pve_host,
|
||||||
pbs_trusted_sources: cfg.pbs_trusted_sources,
|
pbs_trusted_sources: cfg.pbs_trusted_sources,
|
||||||
}
|
}
|
||||||
|
// Flatten per-provider API keys
|
||||||
|
if (cfg.ai_api_keys) {
|
||||||
|
for (const [provider, key] of Object.entries(cfg.ai_api_keys)) {
|
||||||
|
if (key) {
|
||||||
|
flat[`ai_api_key_${provider}`] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ...
|
// Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ...
|
||||||
for (const [chName, chCfg] of Object.entries(cfg.channels)) {
|
for (const [chName, chCfg] of Object.entries(cfg.channels)) {
|
||||||
for (const [field, value] of Object.entries(chCfg)) {
|
for (const [field, value] of Object.entries(chCfg)) {
|
||||||
@@ -633,12 +657,18 @@ export function NotificationSettings() {
|
|||||||
setTestingAI(true)
|
setTestingAI(true)
|
||||||
setAiTestResult(null)
|
setAiTestResult(null)
|
||||||
try {
|
try {
|
||||||
|
// Get the API key for the current provider
|
||||||
|
const currentApiKey = config.ai_api_keys?.[config.ai_provider] || ""
|
||||||
|
// Get the model from provider config (for non-Ollama providers) or from config for Ollama
|
||||||
|
const providerConfig = AI_PROVIDERS.find(p => p.value === config.ai_provider)
|
||||||
|
const modelToUse = config.ai_provider === 'ollama' ? config.ai_model : (providerConfig?.model || config.ai_model)
|
||||||
|
|
||||||
const data = await fetchApi<{ success: boolean; message: string; model: string }>("/api/notifications/test-ai", {
|
const data = await fetchApi<{ success: boolean; message: string; model: string }>("/api/notifications/test-ai", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
provider: config.ai_provider,
|
provider: config.ai_provider,
|
||||||
api_key: config.ai_api_key,
|
api_key: currentApiKey,
|
||||||
model: config.ai_model,
|
model: modelToUse,
|
||||||
ollama_url: config.ai_ollama_url,
|
ollama_url: config.ai_ollama_url,
|
||||||
openai_base_url: config.ai_openai_base_url,
|
openai_base_url: config.ai_openai_base_url,
|
||||||
}),
|
}),
|
||||||
@@ -1504,14 +1534,20 @@ export function NotificationSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
type={showSecrets["ai_key"] ? "text" : "password"}
|
type={showSecrets["ai_key"] ? "text" : "password"}
|
||||||
className="h-9 text-sm font-mono"
|
className="h-9 text-sm font-mono"
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
value={config.ai_api_key}
|
value={config.ai_api_keys?.[config.ai_provider] || ""}
|
||||||
onChange={e => updateConfig(p => ({ ...p, ai_api_key: e.target.value }))}
|
onChange={e => updateConfig(p => ({
|
||||||
disabled={!editMode}
|
...p,
|
||||||
/>
|
ai_api_keys: {
|
||||||
|
...p.ai_api_keys,
|
||||||
|
[p.ai_provider]: e.target.value
|
||||||
|
}
|
||||||
|
}))}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
className="h-9 w-9 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0"
|
className="h-9 w-9 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0"
|
||||||
onClick={() => toggleSecret("ai_key")}
|
onClick={() => toggleSecret("ai_key")}
|
||||||
@@ -1579,7 +1615,7 @@ export function NotificationSettings() {
|
|||||||
{/* Test Connection button */}
|
{/* Test Connection button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleTestAI}
|
onClick={handleTestAI}
|
||||||
disabled={!editMode || testingAI || (config.ai_provider !== "ollama" && !config.ai_api_key)}
|
disabled={!editMode || testingAI || (config.ai_provider !== "ollama" && !config.ai_api_keys?.[config.ai_provider])}
|
||||||
className="w-full h-9 flex items-center justify-center gap-2 rounded-md text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-full h-9 flex items-center justify-center gap-2 rounded-md text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
{testingAI ? (
|
{testingAI ? (
|
||||||
|
|||||||
@@ -52,7 +52,18 @@ SETTINGS_PREFIX = 'notification.'
|
|||||||
ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key')
|
ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key')
|
||||||
|
|
||||||
# Keys that contain sensitive data and should be encrypted
|
# Keys that contain sensitive data and should be encrypted
|
||||||
SENSITIVE_KEYS = {'ai_api_key', 'telegram.token', 'gotify.token', 'discord.webhook_url', 'email.password'}
|
SENSITIVE_KEYS = {
|
||||||
|
'ai_api_key', # Legacy - kept for migration
|
||||||
|
'ai_api_key_groq',
|
||||||
|
'ai_api_key_gemini',
|
||||||
|
'ai_api_key_anthropic',
|
||||||
|
'ai_api_key_openai',
|
||||||
|
'ai_api_key_openrouter',
|
||||||
|
'telegram.token',
|
||||||
|
'gotify.token',
|
||||||
|
'discord.webhook_url',
|
||||||
|
'email.password'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ─── Encryption for Sensitive Data ───────────────────────────────
|
# ─── Encryption for Sensitive Data ───────────────────────────────
|
||||||
@@ -718,10 +729,13 @@ class NotificationManager:
|
|||||||
default_event_enabled = 'true' if template.get('default_enabled', True) else 'false'
|
default_event_enabled = 'true' if template.get('default_enabled', True) else 'false'
|
||||||
|
|
||||||
# Build AI config once (shared across channels, detail_level varies)
|
# Build AI config once (shared across channels, detail_level varies)
|
||||||
|
# Use per-provider API key
|
||||||
|
ai_provider = self._config.get('ai_provider', 'groq')
|
||||||
|
ai_api_key = self._config.get(f'ai_api_key_{ai_provider}', '') or self._config.get('ai_api_key', '')
|
||||||
ai_config = {
|
ai_config = {
|
||||||
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
||||||
'ai_provider': self._config.get('ai_provider', 'groq'),
|
'ai_provider': ai_provider,
|
||||||
'ai_api_key': self._config.get('ai_api_key', ''),
|
'ai_api_key': ai_api_key,
|
||||||
'ai_model': self._config.get('ai_model', ''),
|
'ai_model': self._config.get('ai_model', ''),
|
||||||
'ai_language': self._config.get('ai_language', 'en'),
|
'ai_language': self._config.get('ai_language', 'en'),
|
||||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||||
@@ -1046,11 +1060,13 @@ class NotificationManager:
|
|||||||
message = rendered['body']
|
message = rendered['body']
|
||||||
severity = severity or rendered['severity']
|
severity = severity or rendered['severity']
|
||||||
|
|
||||||
# AI config for enhancement
|
# AI config for enhancement - use per-provider API key
|
||||||
|
ai_provider = self._config.get('ai_provider', 'groq')
|
||||||
|
ai_api_key = self._config.get(f'ai_api_key_{ai_provider}', '') or self._config.get('ai_api_key', '')
|
||||||
ai_config = {
|
ai_config = {
|
||||||
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
||||||
'ai_provider': self._config.get('ai_provider', 'groq'),
|
'ai_provider': ai_provider,
|
||||||
'ai_api_key': self._config.get('ai_api_key', ''),
|
'ai_api_key': ai_api_key,
|
||||||
'ai_model': self._config.get('ai_model', ''),
|
'ai_model': self._config.get('ai_model', ''),
|
||||||
'ai_language': self._config.get('ai_language', 'en'),
|
'ai_language': self._config.get('ai_language', 'en'),
|
||||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||||
@@ -1140,11 +1156,13 @@ class NotificationManager:
|
|||||||
else:
|
else:
|
||||||
return {'success': False, 'error': f'Channel {channel_name} not configured'}
|
return {'success': False, 'error': f'Channel {channel_name} not configured'}
|
||||||
|
|
||||||
# AI config for enhancement
|
# AI config for enhancement - use per-provider API key
|
||||||
|
ai_provider = self._config.get('ai_provider', 'groq')
|
||||||
|
ai_api_key = self._config.get(f'ai_api_key_{ai_provider}', '') or self._config.get('ai_api_key', '')
|
||||||
ai_config = {
|
ai_config = {
|
||||||
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
'ai_enabled': self._config.get('ai_enabled', 'false'),
|
||||||
'ai_provider': self._config.get('ai_provider', 'groq'),
|
'ai_provider': ai_provider,
|
||||||
'ai_api_key': self._config.get('ai_api_key', ''),
|
'ai_api_key': ai_api_key,
|
||||||
'ai_model': self._config.get('ai_model', ''),
|
'ai_model': self._config.get('ai_model', ''),
|
||||||
'ai_language': self._config.get('ai_language', 'en'),
|
'ai_language': self._config.get('ai_language', 'en'),
|
||||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||||
@@ -1153,8 +1171,6 @@ class NotificationManager:
|
|||||||
ai_enabled = self._config.get('ai_enabled', 'false')
|
ai_enabled = self._config.get('ai_enabled', 'false')
|
||||||
if isinstance(ai_enabled, str):
|
if isinstance(ai_enabled, str):
|
||||||
ai_enabled = ai_enabled.lower() == 'true'
|
ai_enabled = ai_enabled.lower() == 'true'
|
||||||
|
|
||||||
ai_provider = self._config.get('ai_provider', 'groq')
|
|
||||||
ai_language = self._config.get('ai_language', 'en')
|
ai_language = self._config.get('ai_language', 'en')
|
||||||
|
|
||||||
# ProxMenux logo for welcome message
|
# ProxMenux logo for welcome message
|
||||||
@@ -1504,6 +1520,42 @@ class NotificationManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[NotificationManager] Failed to migrate AI model: {e}")
|
print(f"[NotificationManager] Failed to migrate AI model: {e}")
|
||||||
|
|
||||||
|
# Get per-provider API keys
|
||||||
|
current_provider = self._config.get('ai_provider', 'groq')
|
||||||
|
ai_api_keys = {
|
||||||
|
'groq': self._config.get('ai_api_key_groq', ''),
|
||||||
|
'gemini': self._config.get('ai_api_key_gemini', ''),
|
||||||
|
'anthropic': self._config.get('ai_api_key_anthropic', ''),
|
||||||
|
'openai': self._config.get('ai_api_key_openai', ''),
|
||||||
|
'openrouter': self._config.get('ai_api_key_openrouter', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Migrate legacy ai_api_key to per-provider key if exists
|
||||||
|
legacy_api_key = self._config.get('ai_api_key', '')
|
||||||
|
if legacy_api_key and not ai_api_keys.get(current_provider):
|
||||||
|
# Migrate legacy key to current provider
|
||||||
|
ai_api_keys[current_provider] = legacy_api_key
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(str(DB_PATH), timeout=10)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Save migrated key
|
||||||
|
migrated_key = encrypt_sensitive_value(legacy_api_key) if legacy_api_key else ''
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT OR REPLACE INTO user_settings (setting_key, setting_value, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (f'{SETTINGS_PREFIX}ai_api_key_{current_provider}', migrated_key, datetime.now().isoformat()))
|
||||||
|
# Clear legacy key
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM user_settings WHERE setting_key = ?
|
||||||
|
''', (f'{SETTINGS_PREFIX}ai_api_key',))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
self._config[f'ai_api_key_{current_provider}'] = legacy_api_key
|
||||||
|
del self._config['ai_api_key']
|
||||||
|
print(f"[NotificationManager] Migrated legacy API key to {current_provider}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[NotificationManager] Failed to migrate legacy API key: {e}")
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
'enabled': self._enabled,
|
'enabled': self._enabled,
|
||||||
'channels': channels,
|
'channels': channels,
|
||||||
@@ -1512,8 +1564,8 @@ class NotificationManager:
|
|||||||
'event_types_by_group': event_types_by_group,
|
'event_types_by_group': event_types_by_group,
|
||||||
'channel_overrides': channel_overrides,
|
'channel_overrides': channel_overrides,
|
||||||
'ai_enabled': self._config.get('ai_enabled', 'false') == 'true',
|
'ai_enabled': self._config.get('ai_enabled', 'false') == 'true',
|
||||||
'ai_provider': self._config.get('ai_provider', 'groq'),
|
'ai_provider': current_provider,
|
||||||
'ai_api_key': self._config.get('ai_api_key', ''),
|
'ai_api_keys': ai_api_keys,
|
||||||
'ai_model': migrated_model,
|
'ai_model': migrated_model,
|
||||||
'ai_language': self._config.get('ai_language', 'en'),
|
'ai_language': self._config.get('ai_language', 'en'),
|
||||||
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
|
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
|
||||||
|
|||||||
Reference in New Issue
Block a user