mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-29 02:46:38 +00:00
Update notification service
This commit is contained in:
@@ -9,13 +9,13 @@ import { Label } from "./ui/label"
|
|||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
|
||||||
import { fetchApi } from "../lib/api-config"
|
import { fetchApi } from "../lib/api-config"
|
||||||
import {
|
import {
|
||||||
Bell, BellOff, Send, CheckCircle2, XCircle, Loader2,
|
Bell, BellOff, Send, CheckCircle2, XCircle, Loader2,
|
||||||
AlertTriangle, Info, Settings2, Zap, Eye, EyeOff,
|
AlertTriangle, Info, Settings2, Zap, Eye, EyeOff,
|
||||||
Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook,
|
Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook,
|
||||||
Copy, Server, Shield, ExternalLink, RefreshCw
|
Copy, Server, Shield, ExternalLink, RefreshCw, Download, Upload
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
interface ChannelConfig {
|
interface ChannelConfig {
|
||||||
@@ -63,6 +63,8 @@ interface NotificationConfig {
|
|||||||
ai_language: string
|
ai_language: string
|
||||||
ai_ollama_url: string
|
ai_ollama_url: string
|
||||||
ai_openai_base_url: string
|
ai_openai_base_url: string
|
||||||
|
ai_prompt_mode: string // 'default' or 'custom'
|
||||||
|
ai_custom_prompt: string // User's custom prompt
|
||||||
channel_ai_detail: Record<string, string>
|
channel_ai_detail: Record<string, string>
|
||||||
hostname: string
|
hostname: string
|
||||||
webhook_secret: string
|
webhook_secret: string
|
||||||
@@ -180,6 +182,23 @@ const AI_DETAIL_LEVELS = [
|
|||||||
{ value: "detailed", label: "Detailed", desc: "Complete technical details" },
|
{ value: "detailed", label: "Detailed", desc: "Complete technical details" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Example custom prompt for users to adapt
|
||||||
|
const EXAMPLE_CUSTOM_PROMPT = `You are a notification formatter for ProxMenux Monitor.
|
||||||
|
|
||||||
|
Your task is to translate and format server notifications.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
1. Translate to the user's language
|
||||||
|
2. Use plain text only (no markdown)
|
||||||
|
3. Be concise and factual
|
||||||
|
4. Do not add recommendations
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
[TITLE]
|
||||||
|
your title here
|
||||||
|
[BODY]
|
||||||
|
your message here`
|
||||||
|
|
||||||
const DEFAULT_CONFIG: NotificationConfig = {
|
const DEFAULT_CONFIG: NotificationConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
channels: {
|
channels: {
|
||||||
@@ -222,6 +241,8 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
ai_language: "en",
|
ai_language: "en",
|
||||||
ai_ollama_url: "http://localhost:11434",
|
ai_ollama_url: "http://localhost:11434",
|
||||||
ai_openai_base_url: "",
|
ai_openai_base_url: "",
|
||||||
|
ai_prompt_mode: "default",
|
||||||
|
ai_custom_prompt: "",
|
||||||
channel_ai_detail: {
|
channel_ai_detail: {
|
||||||
telegram: "brief",
|
telegram: "brief",
|
||||||
gotify: "brief",
|
gotify: "brief",
|
||||||
@@ -259,6 +280,7 @@ export function NotificationSettings() {
|
|||||||
const [aiTestResult, setAiTestResult] = useState<{ success: boolean; message: string; model?: string } | null>(null)
|
const [aiTestResult, setAiTestResult] = useState<{ success: boolean; message: string; model?: string } | null>(null)
|
||||||
const [providerModels, setProviderModels] = useState<string[]>([])
|
const [providerModels, setProviderModels] = useState<string[]>([])
|
||||||
const [loadingProviderModels, setLoadingProviderModels] = useState(false)
|
const [loadingProviderModels, setLoadingProviderModels] = useState(false)
|
||||||
|
const [showCustomPromptInfo, setShowCustomPromptInfo] = useState(false)
|
||||||
const [webhookSetup, setWebhookSetup] = useState<{
|
const [webhookSetup, setWebhookSetup] = useState<{
|
||||||
status: "idle" | "running" | "success" | "failed"
|
status: "idle" | "running" | "success" | "failed"
|
||||||
fallback_commands: string[]
|
fallback_commands: string[]
|
||||||
@@ -269,7 +291,7 @@ export function NotificationSettings() {
|
|||||||
try {
|
try {
|
||||||
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) {
|
||||||
// Ensure ai_api_keys and ai_models objects exist (fallback for older configs)
|
// Ensure ai_api_keys, ai_models, and prompt settings exist (fallback for older configs)
|
||||||
const configWithDefaults = {
|
const configWithDefaults = {
|
||||||
...data.config,
|
...data.config,
|
||||||
ai_api_keys: data.config.ai_api_keys || {
|
ai_api_keys: data.config.ai_api_keys || {
|
||||||
@@ -287,7 +309,9 @@ export function NotificationSettings() {
|
|||||||
anthropic: "",
|
anthropic: "",
|
||||||
openai: "",
|
openai: "",
|
||||||
openrouter: "",
|
openrouter: "",
|
||||||
}
|
},
|
||||||
|
ai_prompt_mode: data.config.ai_prompt_mode || "default",
|
||||||
|
ai_custom_prompt: data.config.ai_custom_prompt || "",
|
||||||
}
|
}
|
||||||
// If ai_model exists but ai_models doesn't have it, save it
|
// If ai_model exists but ai_models doesn't have it, save it
|
||||||
if (configWithDefaults.ai_model && !configWithDefaults.ai_models[configWithDefaults.ai_provider]) {
|
if (configWithDefaults.ai_model && !configWithDefaults.ai_models[configWithDefaults.ai_provider]) {
|
||||||
@@ -1669,80 +1693,173 @@ export function NotificationSettings() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Connection button */}
|
{/* Prompt Mode section */}
|
||||||
<button
|
|
||||||
onClick={handleTestAI}
|
|
||||||
disabled={
|
|
||||||
!editMode ||
|
|
||||||
testingAI ||
|
|
||||||
!config.ai_model ||
|
|
||||||
(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"
|
|
||||||
>
|
|
||||||
{testingAI ? (
|
|
||||||
<><Loader2 className="h-4 w-4 animate-spin" /> Testing...</>
|
|
||||||
) : (
|
|
||||||
<><Zap className="h-4 w-4" /> Test Connection</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Test result */}
|
|
||||||
{aiTestResult && (
|
|
||||||
<div className={`flex items-start gap-2 p-3 rounded-md ${
|
|
||||||
aiTestResult.success
|
|
||||||
? "bg-green-500/10 border border-green-500/20"
|
|
||||||
: "bg-red-500/10 border border-red-500/20"
|
|
||||||
}`}>
|
|
||||||
{aiTestResult.success
|
|
||||||
? <CheckCircle2 className="h-4 w-4 text-green-400 shrink-0 mt-0.5" />
|
|
||||||
: <XCircle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
|
||||||
}
|
|
||||||
<p className={`text-xs sm:text-sm leading-relaxed ${
|
|
||||||
aiTestResult.success ? "text-green-400/90" : "text-red-400/90"
|
|
||||||
}`}>
|
|
||||||
{aiTestResult.message}
|
|
||||||
{aiTestResult.model && ` (${aiTestResult.model})`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Per-channel detail level */}
|
|
||||||
<div className="space-y-3 pt-3 border-t border-border/50">
|
<div className="space-y-3 pt-3 border-t border-border/50">
|
||||||
<Label className="text-xs sm:text-sm text-foreground/80">Detail Level per Channel</Label>
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<Label className="text-xs sm:text-sm text-foreground/80">Prompt Mode</Label>
|
||||||
{CHANNEL_TYPES.map(ch => (
|
<Select
|
||||||
<div key={ch} className="flex items-center justify-between gap-2 px-3 py-2 rounded bg-muted/30">
|
value={config.ai_prompt_mode || "default"}
|
||||||
<span className="text-xs sm:text-sm text-foreground/70 capitalize">{ch}</span>
|
onValueChange={v => {
|
||||||
<Select
|
updateConfig(p => ({ ...p, ai_prompt_mode: v }))
|
||||||
value={config.channel_ai_detail?.[ch] || "standard"}
|
// Show info modal when switching to custom for the first time
|
||||||
onValueChange={v => updateConfig(p => ({
|
if (v === "custom" && !config.ai_custom_prompt) {
|
||||||
...p,
|
setShowCustomPromptInfo(true)
|
||||||
channel_ai_detail: { ...p.channel_ai_detail, [ch]: v }
|
}
|
||||||
}))}
|
}}
|
||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 w-[90px] text-xs px-2">
|
<SelectTrigger className="h-9 text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{AI_DETAIL_LEVELS.map(l => (
|
<SelectItem value="default">Default Prompt</SelectItem>
|
||||||
<SelectItem key={l.value} value={l.value} className="text-xs">
|
<SelectItem value="custom">Custom Prompt</SelectItem>
|
||||||
{l.label}
|
</SelectContent>
|
||||||
</SelectItem>
|
</Select>
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Default mode: Detail Level per Channel */}
|
||||||
|
{(config.ai_prompt_mode || "default") === "default" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-xs sm:text-sm text-foreground/80">Detail Level per Channel</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{CHANNEL_TYPES.map(ch => (
|
||||||
|
<div key={ch} className="flex items-center justify-between gap-2 px-3 py-2 rounded bg-muted/30">
|
||||||
|
<span className="text-xs sm:text-sm text-foreground/70 capitalize">{ch}</span>
|
||||||
|
<Select
|
||||||
|
value={config.channel_ai_detail?.[ch] || "standard"}
|
||||||
|
onValueChange={v => updateConfig(p => ({
|
||||||
|
...p,
|
||||||
|
channel_ai_detail: { ...p.channel_ai_detail, [ch]: v }
|
||||||
|
}))}
|
||||||
|
disabled={!editMode}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 w-[90px] text-xs px-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{AI_DETAIL_LEVELS.map(l => (
|
||||||
|
<SelectItem key={l.value} value={l.value} className="text-xs">
|
||||||
|
{l.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
||||||
|
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
||||||
|
AI translates and formats notifications to your selected language. Each channel can have different detail levels.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom mode: Editable prompt textarea */}
|
||||||
|
{config.ai_prompt_mode === "custom" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm text-foreground/80">Custom Prompt</Label>
|
||||||
|
<textarea
|
||||||
|
value={config.ai_custom_prompt || ""}
|
||||||
|
onChange={e => updateConfig(p => ({ ...p, ai_custom_prompt: e.target.value }))}
|
||||||
|
disabled={!editMode}
|
||||||
|
placeholder="Enter your custom prompt instructions for the AI..."
|
||||||
|
className="w-full h-48 px-3 py-2 text-sm rounded-md border border-border bg-background resize-y focus:outline-none focus:ring-2 focus:ring-purple-500/50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!editMode}
|
||||||
|
onClick={() => {
|
||||||
|
const blob = new Blob([config.ai_custom_prompt || ""], { type: "text/plain" })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = "proxmenux_custom_prompt.txt"
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!editMode}
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.createElement("input")
|
||||||
|
input.type = "file"
|
||||||
|
input.accept = ".txt,.md"
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (file) {
|
||||||
|
const text = await file.text()
|
||||||
|
updateConfig(p => ({ ...p, ai_custom_prompt: text }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
||||||
|
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
||||||
|
Define your own prompt rules and format. You control the detail level and style of all notifications. Export to share with others or import prompts from the community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
{/* Test Connection button - moved to end */}
|
||||||
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
<div className="space-y-3 pt-3 border-t border-border/50">
|
||||||
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
<button
|
||||||
AI enhancement translates and formats notifications to your selected language. Each channel can have different detail levels. If the AI service is unavailable, standard templates are used as fallback.
|
onClick={handleTestAI}
|
||||||
</p>
|
disabled={
|
||||||
|
!editMode ||
|
||||||
|
testingAI ||
|
||||||
|
!config.ai_model ||
|
||||||
|
(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"
|
||||||
|
>
|
||||||
|
{testingAI ? (
|
||||||
|
<><Loader2 className="h-4 w-4 animate-spin" /> Testing...</>
|
||||||
|
) : (
|
||||||
|
<><Zap className="h-4 w-4" /> Test Connection</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Test result */}
|
||||||
|
{aiTestResult && (
|
||||||
|
<div className={`flex items-start gap-2 p-3 rounded-md ${
|
||||||
|
aiTestResult.success
|
||||||
|
? "bg-green-500/10 border border-green-500/20"
|
||||||
|
: "bg-red-500/10 border border-red-500/20"
|
||||||
|
}`}>
|
||||||
|
{aiTestResult.success
|
||||||
|
? <CheckCircle2 className="h-4 w-4 text-green-400 shrink-0 mt-0.5" />
|
||||||
|
: <XCircle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
||||||
|
}
|
||||||
|
<p className={`text-xs sm:text-sm leading-relaxed ${
|
||||||
|
aiTestResult.success ? "text-green-400/90" : "text-red-400/90"
|
||||||
|
}`}>
|
||||||
|
{aiTestResult.message}
|
||||||
|
{aiTestResult.model && ` (${aiTestResult.model})`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1903,6 +2020,77 @@ export function NotificationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Custom Prompt Info Modal */}
|
||||||
|
<Dialog open={showCustomPromptInfo} onOpenChange={setShowCustomPromptInfo}>
|
||||||
|
<DialogContent className="max-w-[90vw] sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Settings2 className="h-5 w-5 text-purple-400" />
|
||||||
|
Custom Prompt Mode
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
Create your own AI prompt for ProxMenux Monitor notifications
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-foreground/90">What is a custom prompt?</h4>
|
||||||
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
|
The prompt defines how the AI formats your notifications. With a custom prompt, you control the style, detail level, and format of all messages.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-foreground/90">Important requirements</h4>
|
||||||
|
<ul className="text-muted-foreground text-xs space-y-1.5">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-purple-400 mt-0.5">1.</span>
|
||||||
|
<span>Your prompt must output in this format:<br/>
|
||||||
|
<code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">[TITLE]</code> followed by the title, then <code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">[BODY]</code> followed by the message
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-purple-400 mt-0.5">2.</span>
|
||||||
|
<span>Use plain text only (no markdown) for compatibility with all channels</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-purple-400 mt-0.5">3.</span>
|
||||||
|
<span>The prompt receives raw Proxmox event data as input</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-foreground/90">Getting started</h4>
|
||||||
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
|
We have added an example prompt to get you started. You can adapt it, export it to share with others, or import prompts from the community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
updateConfig(p => ({ ...p, ai_custom_prompt: EXAMPLE_CUSTOM_PROMPT }))
|
||||||
|
setShowCustomPromptInfo(false)
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Load Example
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowCustomPromptInfo(false)}
|
||||||
|
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
38
AppImage/config/default_prompt.txt
Normal file
38
AppImage/config/default_prompt.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
You are a system notification formatter for ProxMenux Monitor, a Proxmox VE monitoring tool.
|
||||||
|
|
||||||
|
Your task is to translate and reformat incoming server alert messages into {language}.
|
||||||
|
|
||||||
|
═══ ABSOLUTE RULES ═══
|
||||||
|
1. Translate BOTH title and body to {language}. Every word, label, and unit must be in {language}.
|
||||||
|
2. NO markdown: no **bold**, no *italic*, no `code`, no headers (#), no bullet lists (- or *)
|
||||||
|
3. Plain text only — the output is sent to chat apps and email which handle their own formatting
|
||||||
|
4. Tone: factual, concise, technical. No greetings, no closings, no apologies
|
||||||
|
5. DO NOT add recommendations, action items, or suggestions ("you should…", "consider…")
|
||||||
|
6. Present ONLY the facts already in the input — do not invent or assume information
|
||||||
|
7. OUTPUT ONLY THE FINAL RESULT — never include both original and processed versions.
|
||||||
|
Do NOT append "Original message:", "Original:", "Source:", or any before/after comparison.
|
||||||
|
Return ONLY the single, final formatted message in {language}.
|
||||||
|
8. PLAIN NARRATIVE LINES — if a line in the input is a complete sentence (not a "Label: value"
|
||||||
|
pair), translate it as-is. Never prepend "Message:", "Note:", or any other label to a sentence.
|
||||||
|
9. Detail level to apply: {detail_level}
|
||||||
|
- brief → 2-3 lines, essential data only (status + key metric)
|
||||||
|
- standard → short paragraph covering who/what/where and the key value
|
||||||
|
- detailed → full technical breakdown of all available fields
|
||||||
|
10. Keep the "hostname: " prefix in the title. Translate only the descriptive part.
|
||||||
|
Example: "pve01: Updates available" → "pve01: Actualizaciones disponibles"
|
||||||
|
11. EMPTY LIST VALUES — if a list field is empty, "none", or "0":
|
||||||
|
Always write the translated word for "none" on the line after the label, never leave it blank.
|
||||||
|
12. DEDUPLICATION — input may contain redundant or repeated information from multiple monitoring sources:
|
||||||
|
- Identify and merge duplicate facts (same device, same error, same metric mentioned twice)
|
||||||
|
- Present each unique fact exactly once in a clear, consolidated form
|
||||||
|
- If the same data appears in different formats, choose the most informative version
|
||||||
|
|
||||||
|
{emoji_instructions}
|
||||||
|
|
||||||
|
═══ OUTPUT FORMAT ═══
|
||||||
|
TITLE: <translated title>
|
||||||
|
BODY:
|
||||||
|
<translated body>
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
- Do NOT include the literal words TITLE or BODY anywhere in the translated content
|
||||||
@@ -110,12 +110,13 @@ else
|
|||||||
echo "⚠️ ai_providers directory not found"
|
echo "⚠️ ai_providers directory not found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy config files (verified AI models, etc.)
|
# Copy config files (verified AI models, prompts, etc.)
|
||||||
echo "📋 Copying config files..."
|
echo "📋 Copying config files..."
|
||||||
CONFIG_DIR="$APPIMAGE_ROOT/config"
|
CONFIG_DIR="$APPIMAGE_ROOT/config"
|
||||||
if [ -d "$CONFIG_DIR" ]; then
|
if [ -d "$CONFIG_DIR" ]; then
|
||||||
mkdir -p "$APP_DIR/usr/bin/config"
|
mkdir -p "$APP_DIR/usr/bin/config"
|
||||||
cp "$CONFIG_DIR/"*.json "$APP_DIR/usr/bin/config/" 2>/dev/null || true
|
cp "$CONFIG_DIR/"*.json "$APP_DIR/usr/bin/config/" 2>/dev/null || true
|
||||||
|
cp "$CONFIG_DIR/"*.txt "$APP_DIR/usr/bin/config/" 2>/dev/null || true
|
||||||
echo "✅ Config files copied"
|
echo "✅ Config files copied"
|
||||||
else
|
else
|
||||||
echo "⚠️ config directory not found"
|
echo "⚠️ config directory not found"
|
||||||
|
|||||||
@@ -739,6 +739,8 @@ class NotificationManager:
|
|||||||
'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', ''),
|
||||||
|
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||||
|
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get journal context if available
|
# Get journal context if available
|
||||||
@@ -1070,6 +1072,8 @@ class NotificationManager:
|
|||||||
'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', ''),
|
||||||
|
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||||
|
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
@@ -1166,6 +1170,8 @@ class NotificationManager:
|
|||||||
'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', ''),
|
||||||
|
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||||
|
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
ai_enabled = self._config.get('ai_enabled', 'false')
|
ai_enabled = self._config.get('ai_enabled', 'false')
|
||||||
@@ -1559,6 +1565,8 @@ class NotificationManager:
|
|||||||
'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'),
|
||||||
'ai_openai_base_url': self._config.get('ai_openai_base_url', ''),
|
'ai_openai_base_url': self._config.get('ai_openai_base_url', ''),
|
||||||
|
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||||
|
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||||
'ai_detail_levels': ai_detail_levels,
|
'ai_detail_levels': ai_detail_levels,
|
||||||
'hostname': self._config.get('hostname', ''),
|
'hostname': self._config.get('hostname', ''),
|
||||||
'webhook_secret': self._config.get('webhook_secret', ''),
|
'webhook_secret': self._config.get('webhook_secret', ''),
|
||||||
|
|||||||
@@ -1682,18 +1682,23 @@ class AIEnhancer:
|
|||||||
language_code = self.config.get('ai_language', 'en')
|
language_code = self.config.get('ai_language', 'en')
|
||||||
language_name = AI_LANGUAGES.get(language_code, 'English')
|
language_name = AI_LANGUAGES.get(language_code, 'English')
|
||||||
|
|
||||||
# Get token limit for detail level
|
# Check for custom prompt mode
|
||||||
max_tokens = AI_DETAIL_TOKENS.get(detail_level, 200)
|
prompt_mode = self.config.get('ai_prompt_mode', 'default')
|
||||||
|
custom_prompt = self.config.get('ai_custom_prompt', '')
|
||||||
|
|
||||||
# Select emoji instructions based on channel type
|
if prompt_mode == 'custom' and custom_prompt.strip():
|
||||||
emoji_instructions = AI_EMOJI_INSTRUCTIONS if use_emojis else AI_NO_EMOJI_INSTRUCTIONS
|
# Custom prompt: user controls everything, use higher token limit
|
||||||
|
system_prompt = custom_prompt
|
||||||
# Build system prompt with emoji instructions
|
max_tokens = 500 # Allow more tokens for custom prompts
|
||||||
system_prompt = AI_SYSTEM_PROMPT.format(
|
else:
|
||||||
language=language_name,
|
# Default prompt: use detail level and emoji settings
|
||||||
detail_level=detail_level,
|
max_tokens = AI_DETAIL_TOKENS.get(detail_level, 200)
|
||||||
emoji_instructions=emoji_instructions
|
emoji_instructions = AI_EMOJI_INSTRUCTIONS if use_emojis else AI_NO_EMOJI_INSTRUCTIONS
|
||||||
)
|
system_prompt = AI_SYSTEM_PROMPT.format(
|
||||||
|
language=language_name,
|
||||||
|
detail_level=detail_level,
|
||||||
|
emoji_instructions=emoji_instructions
|
||||||
|
)
|
||||||
|
|
||||||
# Build user message
|
# Build user message
|
||||||
user_msg = f"Severity: {severity}\nTitle: {title}\nMessage:\n{body}"
|
user_msg = f"Severity: {severity}\nTitle: {title}\nMessage:\n{body}"
|
||||||
|
|||||||
Reference in New Issue
Block a user