mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-13 01:32:40 +02:00
Add Gotify notification support
- Add gotify_url, gotify_app_token, gotify_enabled columns to users table - Add sendGotifyNotification function with priority levels - Add testGotifyConnection function to verify server connectivity - Add test-gotify endpoint for connection testing before save - Add Gotify section in Settings with: - Server URL input with Test Connection button - App Token input (masked) - Enable/disable toggle - Send Test button (after configured) - Include Gotify in notification dispatching Gotify is a self-hosted push notification server popular in the self-hosted community, complementing the existing ntfy support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5e850aee18
commit
d9374c2f57
6 changed files with 355 additions and 3 deletions
|
|
@ -119,6 +119,15 @@ async function runMigrations() {
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ntfy_enabled') THEN
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ntfy_enabled') THEN
|
||||||
ALTER TABLE users ADD COLUMN ntfy_enabled BOOLEAN DEFAULT true;
|
ALTER TABLE users ADD COLUMN ntfy_enabled BOOLEAN DEFAULT true;
|
||||||
END IF;
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'gotify_url') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN gotify_url TEXT;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'gotify_app_token') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN gotify_app_token TEXT;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'gotify_enabled') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN gotify_enabled BOOLEAN DEFAULT true;
|
||||||
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ai_verification_enabled') THEN
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ai_verification_enabled') THEN
|
||||||
ALTER TABLE users ADD COLUMN ai_verification_enabled BOOLEAN DEFAULT false;
|
ALTER TABLE users ADD COLUMN ai_verification_enabled BOOLEAN DEFAULT false;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ export interface NotificationSettings {
|
||||||
pushover_enabled: boolean;
|
pushover_enabled: boolean;
|
||||||
ntfy_topic: string | null;
|
ntfy_topic: string | null;
|
||||||
ntfy_enabled: boolean;
|
ntfy_enabled: boolean;
|
||||||
|
gotify_url: string | null;
|
||||||
|
gotify_app_token: string | null;
|
||||||
|
gotify_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AISettings {
|
export interface AISettings {
|
||||||
|
|
@ -76,7 +79,8 @@ export const userQueries = {
|
||||||
`SELECT telegram_bot_token, telegram_chat_id, COALESCE(telegram_enabled, true) as telegram_enabled,
|
`SELECT telegram_bot_token, telegram_chat_id, COALESCE(telegram_enabled, true) as telegram_enabled,
|
||||||
discord_webhook_url, COALESCE(discord_enabled, true) as discord_enabled,
|
discord_webhook_url, COALESCE(discord_enabled, true) as discord_enabled,
|
||||||
pushover_user_key, pushover_app_token, COALESCE(pushover_enabled, true) as pushover_enabled,
|
pushover_user_key, pushover_app_token, COALESCE(pushover_enabled, true) as pushover_enabled,
|
||||||
ntfy_topic, COALESCE(ntfy_enabled, true) as ntfy_enabled
|
ntfy_topic, COALESCE(ntfy_enabled, true) as ntfy_enabled,
|
||||||
|
gotify_url, gotify_app_token, COALESCE(gotify_enabled, true) as gotify_enabled
|
||||||
FROM users WHERE id = $1`,
|
FROM users WHERE id = $1`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
@ -131,6 +135,18 @@ export const userQueries = {
|
||||||
fields.push(`ntfy_enabled = $${paramIndex++}`);
|
fields.push(`ntfy_enabled = $${paramIndex++}`);
|
||||||
values.push(settings.ntfy_enabled);
|
values.push(settings.ntfy_enabled);
|
||||||
}
|
}
|
||||||
|
if (settings.gotify_url !== undefined) {
|
||||||
|
fields.push(`gotify_url = $${paramIndex++}`);
|
||||||
|
values.push(settings.gotify_url);
|
||||||
|
}
|
||||||
|
if (settings.gotify_app_token !== undefined) {
|
||||||
|
fields.push(`gotify_app_token = $${paramIndex++}`);
|
||||||
|
values.push(settings.gotify_app_token);
|
||||||
|
}
|
||||||
|
if (settings.gotify_enabled !== undefined) {
|
||||||
|
fields.push(`gotify_enabled = $${paramIndex++}`);
|
||||||
|
values.push(settings.gotify_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.length === 0) return null;
|
if (fields.length === 0) return null;
|
||||||
|
|
||||||
|
|
@ -140,7 +156,8 @@ export const userQueries = {
|
||||||
RETURNING telegram_bot_token, telegram_chat_id, COALESCE(telegram_enabled, true) as telegram_enabled,
|
RETURNING telegram_bot_token, telegram_chat_id, COALESCE(telegram_enabled, true) as telegram_enabled,
|
||||||
discord_webhook_url, COALESCE(discord_enabled, true) as discord_enabled,
|
discord_webhook_url, COALESCE(discord_enabled, true) as discord_enabled,
|
||||||
pushover_user_key, pushover_app_token, COALESCE(pushover_enabled, true) as pushover_enabled,
|
pushover_user_key, pushover_app_token, COALESCE(pushover_enabled, true) as pushover_enabled,
|
||||||
ntfy_topic, COALESCE(ntfy_enabled, true) as ntfy_enabled`,
|
ntfy_topic, COALESCE(ntfy_enabled, true) as ntfy_enabled,
|
||||||
|
gotify_url, gotify_app_token, COALESCE(gotify_enabled, true) as gotify_enabled`,
|
||||||
values
|
values
|
||||||
);
|
);
|
||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ router.get('/notifications', async (req: AuthRequest, res: Response) => {
|
||||||
pushover_enabled: settings.pushover_enabled ?? true,
|
pushover_enabled: settings.pushover_enabled ?? true,
|
||||||
ntfy_topic: settings.ntfy_topic || null,
|
ntfy_topic: settings.ntfy_topic || null,
|
||||||
ntfy_enabled: settings.ntfy_enabled ?? true,
|
ntfy_enabled: settings.ntfy_enabled ?? true,
|
||||||
|
gotify_url: settings.gotify_url || null,
|
||||||
|
gotify_app_token: settings.gotify_app_token || null,
|
||||||
|
gotify_enabled: settings.gotify_enabled ?? true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching notification settings:', error);
|
console.error('Error fetching notification settings:', error);
|
||||||
|
|
@ -51,6 +54,9 @@ router.put('/notifications', async (req: AuthRequest, res: Response) => {
|
||||||
pushover_enabled,
|
pushover_enabled,
|
||||||
ntfy_topic,
|
ntfy_topic,
|
||||||
ntfy_enabled,
|
ntfy_enabled,
|
||||||
|
gotify_url,
|
||||||
|
gotify_app_token,
|
||||||
|
gotify_enabled,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const settings = await userQueries.updateNotificationSettings(userId, {
|
const settings = await userQueries.updateNotificationSettings(userId, {
|
||||||
|
|
@ -64,6 +70,9 @@ router.put('/notifications', async (req: AuthRequest, res: Response) => {
|
||||||
pushover_enabled,
|
pushover_enabled,
|
||||||
ntfy_topic,
|
ntfy_topic,
|
||||||
ntfy_enabled,
|
ntfy_enabled,
|
||||||
|
gotify_url,
|
||||||
|
gotify_app_token,
|
||||||
|
gotify_enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
|
|
@ -82,6 +91,9 @@ router.put('/notifications', async (req: AuthRequest, res: Response) => {
|
||||||
pushover_enabled: settings.pushover_enabled ?? true,
|
pushover_enabled: settings.pushover_enabled ?? true,
|
||||||
ntfy_topic: settings.ntfy_topic || null,
|
ntfy_topic: settings.ntfy_topic || null,
|
||||||
ntfy_enabled: settings.ntfy_enabled ?? true,
|
ntfy_enabled: settings.ntfy_enabled ?? true,
|
||||||
|
gotify_url: settings.gotify_url || null,
|
||||||
|
gotify_app_token: settings.gotify_app_token || null,
|
||||||
|
gotify_enabled: settings.gotify_enabled ?? true,
|
||||||
message: 'Notification settings updated successfully',
|
message: 'Notification settings updated successfully',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -226,6 +238,66 @@ router.post('/notifications/test/ntfy', async (req: AuthRequest, res: Response)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test Gotify connection (before saving)
|
||||||
|
router.post('/notifications/test-gotify', async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { url, app_token } = req.body;
|
||||||
|
|
||||||
|
if (!url || !app_token) {
|
||||||
|
res.status(400).json({ error: 'Server URL and app token are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { testGotifyConnection } = await import('../services/notifications');
|
||||||
|
const result = await testGotifyConnection(url, app_token);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ success: true, message: 'Successfully connected to Gotify server' });
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ success: false, error: result.error });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error testing Gotify connection:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to test Gotify connection' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Gotify notification (after saving)
|
||||||
|
router.post('/notifications/test/gotify', async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId!;
|
||||||
|
const settings = await userQueries.getNotificationSettings(userId);
|
||||||
|
|
||||||
|
if (!settings?.gotify_url || !settings?.gotify_app_token) {
|
||||||
|
res.status(400).json({ error: 'Gotify not configured' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sendGotifyNotification } = await import('../services/notifications');
|
||||||
|
const success = await sendGotifyNotification(
|
||||||
|
settings.gotify_url,
|
||||||
|
settings.gotify_app_token,
|
||||||
|
{
|
||||||
|
productName: 'Test Product',
|
||||||
|
productUrl: 'https://example.com',
|
||||||
|
type: 'price_drop',
|
||||||
|
oldPrice: 29.99,
|
||||||
|
newPrice: 19.99,
|
||||||
|
currency: 'USD',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
res.json({ message: 'Test notification sent successfully' });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Failed to send test notification' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending test Gotify notification:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to send test notification' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get AI settings
|
// Get AI settings
|
||||||
router.get('/ai', async (req: AuthRequest, res: Response) => {
|
router.get('/ai', async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,83 @@ export async function sendNtfyNotification(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendGotifyNotification(
|
||||||
|
serverUrl: string,
|
||||||
|
appToken: string,
|
||||||
|
payload: NotificationPayload
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const currencySymbol = getCurrencySymbol(payload.currency);
|
||||||
|
|
||||||
|
let title: string;
|
||||||
|
let message: string;
|
||||||
|
let priority: number;
|
||||||
|
|
||||||
|
if (payload.type === 'price_drop') {
|
||||||
|
const oldPriceStr = payload.oldPrice ? `${currencySymbol}${payload.oldPrice.toFixed(2)}` : 'N/A';
|
||||||
|
const newPriceStr = payload.newPrice ? `${currencySymbol}${payload.newPrice.toFixed(2)}` : 'N/A';
|
||||||
|
title = 'Price Drop Alert!';
|
||||||
|
message = `${payload.productName}\n\nPrice dropped from ${oldPriceStr} to ${newPriceStr}\n\n${payload.productUrl}`;
|
||||||
|
priority = 7; // High priority
|
||||||
|
} else if (payload.type === 'target_price') {
|
||||||
|
const newPriceStr = payload.newPrice ? `${currencySymbol}${payload.newPrice.toFixed(2)}` : 'N/A';
|
||||||
|
const targetPriceStr = payload.targetPrice ? `${currencySymbol}${payload.targetPrice.toFixed(2)}` : 'N/A';
|
||||||
|
title = 'Target Price Reached!';
|
||||||
|
message = `${payload.productName}\n\nPrice is now ${newPriceStr} (your target: ${targetPriceStr})\n\n${payload.productUrl}`;
|
||||||
|
priority = 8; // Higher priority
|
||||||
|
} else {
|
||||||
|
const priceStr = payload.newPrice ? ` at ${currencySymbol}${payload.newPrice.toFixed(2)}` : '';
|
||||||
|
title = 'Back in Stock!';
|
||||||
|
message = `${payload.productName}\n\nThis item is now available${priceStr}\n\n${payload.productUrl}`;
|
||||||
|
priority = 8; // Higher priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gotify API: POST /message with token as query param or header
|
||||||
|
const url = `${serverUrl.replace(/\/$/, '')}/message`;
|
||||||
|
await axios.post(url, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'X-Gotify-Key': appToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Gotify notification sent');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send Gotify notification:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testGotifyConnection(
|
||||||
|
serverUrl: string,
|
||||||
|
appToken: string
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// Test by fetching application info
|
||||||
|
const url = `${serverUrl.replace(/\/$/, '')}/application`;
|
||||||
|
await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'X-Gotify-Key': appToken,
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
if (errorMessage.includes('ECONNREFUSED')) {
|
||||||
|
return { success: false, error: 'Cannot connect to Gotify server. Make sure it is running.' };
|
||||||
|
}
|
||||||
|
if (errorMessage.includes('401') || errorMessage.includes('403')) {
|
||||||
|
return { success: false, error: 'Invalid app token. Check your Gotify application token.' };
|
||||||
|
}
|
||||||
|
return { success: false, error: `Connection failed: ${errorMessage}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationResult {
|
export interface NotificationResult {
|
||||||
channelsNotified: string[];
|
channelsNotified: string[];
|
||||||
channelsFailed: string[];
|
channelsFailed: string[];
|
||||||
|
|
@ -258,6 +335,9 @@ export async function sendNotifications(
|
||||||
pushover_enabled?: boolean;
|
pushover_enabled?: boolean;
|
||||||
ntfy_topic: string | null;
|
ntfy_topic: string | null;
|
||||||
ntfy_enabled?: boolean;
|
ntfy_enabled?: boolean;
|
||||||
|
gotify_url: string | null;
|
||||||
|
gotify_app_token: string | null;
|
||||||
|
gotify_enabled?: boolean;
|
||||||
},
|
},
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<NotificationResult> {
|
): Promise<NotificationResult> {
|
||||||
|
|
@ -292,6 +372,13 @@ export async function sendNotifications(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.gotify_url && settings.gotify_app_token && settings.gotify_enabled !== false) {
|
||||||
|
channelPromises.push({
|
||||||
|
channel: 'gotify',
|
||||||
|
promise: sendGotifyNotification(settings.gotify_url, settings.gotify_app_token, payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const results = await Promise.allSettled(channelPromises.map(c => c.promise));
|
const results = await Promise.allSettled(channelPromises.map(c => c.promise));
|
||||||
|
|
||||||
const channelsNotified: string[] = [];
|
const channelsNotified: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,9 @@ export interface NotificationSettings {
|
||||||
pushover_enabled: boolean;
|
pushover_enabled: boolean;
|
||||||
ntfy_topic: string | null;
|
ntfy_topic: string | null;
|
||||||
ntfy_enabled: boolean;
|
ntfy_enabled: boolean;
|
||||||
|
gotify_url: string | null;
|
||||||
|
gotify_app_token: string | null;
|
||||||
|
gotify_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsApi = {
|
export const settingsApi = {
|
||||||
|
|
@ -208,6 +211,9 @@ export const settingsApi = {
|
||||||
pushover_enabled?: boolean;
|
pushover_enabled?: boolean;
|
||||||
ntfy_topic?: string | null;
|
ntfy_topic?: string | null;
|
||||||
ntfy_enabled?: boolean;
|
ntfy_enabled?: boolean;
|
||||||
|
gotify_url?: string | null;
|
||||||
|
gotify_app_token?: string | null;
|
||||||
|
gotify_enabled?: boolean;
|
||||||
}) => api.put<NotificationSettings & { message: string }>('/settings/notifications', data),
|
}) => api.put<NotificationSettings & { message: string }>('/settings/notifications', data),
|
||||||
|
|
||||||
testTelegram: () =>
|
testTelegram: () =>
|
||||||
|
|
@ -222,6 +228,15 @@ export const settingsApi = {
|
||||||
testNtfy: () =>
|
testNtfy: () =>
|
||||||
api.post<{ message: string }>('/settings/notifications/test/ntfy'),
|
api.post<{ message: string }>('/settings/notifications/test/ntfy'),
|
||||||
|
|
||||||
|
testGotifyConnection: (url: string, appToken: string) =>
|
||||||
|
api.post<{ success: boolean; message?: string; error?: string }>('/settings/notifications/test-gotify', {
|
||||||
|
url,
|
||||||
|
app_token: appToken,
|
||||||
|
}),
|
||||||
|
|
||||||
|
testGotify: () =>
|
||||||
|
api.post<{ message: string }>('/settings/notifications/test/gotify'),
|
||||||
|
|
||||||
// AI Settings
|
// AI Settings
|
||||||
getAI: () =>
|
getAI: () =>
|
||||||
api.get<AISettings>('/settings/ai'),
|
api.get<AISettings>('/settings/ai'),
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,12 @@ export default function Settings() {
|
||||||
const [pushoverEnabled, setPushoverEnabled] = useState(true);
|
const [pushoverEnabled, setPushoverEnabled] = useState(true);
|
||||||
const [ntfyTopic, setNtfyTopic] = useState('');
|
const [ntfyTopic, setNtfyTopic] = useState('');
|
||||||
const [ntfyEnabled, setNtfyEnabled] = useState(true);
|
const [ntfyEnabled, setNtfyEnabled] = useState(true);
|
||||||
|
const [gotifyUrl, setGotifyUrl] = useState('');
|
||||||
|
const [gotifyAppToken, setGotifyAppToken] = useState('');
|
||||||
|
const [gotifyEnabled, setGotifyEnabled] = useState(true);
|
||||||
|
const [isTestingGotify, setIsTestingGotify] = useState(false);
|
||||||
const [isSavingNotifications, setIsSavingNotifications] = useState(false);
|
const [isSavingNotifications, setIsSavingNotifications] = useState(false);
|
||||||
const [isTesting, setIsTesting] = useState<'telegram' | 'discord' | 'pushover' | 'ntfy' | null>(null);
|
const [isTesting, setIsTesting] = useState<'telegram' | 'discord' | 'pushover' | 'ntfy' | 'gotify' | null>(null);
|
||||||
|
|
||||||
// AI state
|
// AI state
|
||||||
const [aiSettings, setAISettings] = useState<AISettings | null>(null);
|
const [aiSettings, setAISettings] = useState<AISettings | null>(null);
|
||||||
|
|
@ -108,6 +112,9 @@ export default function Settings() {
|
||||||
setPushoverEnabled(notificationsRes.data.pushover_enabled ?? true);
|
setPushoverEnabled(notificationsRes.data.pushover_enabled ?? true);
|
||||||
setNtfyTopic(notificationsRes.data.ntfy_topic || '');
|
setNtfyTopic(notificationsRes.data.ntfy_topic || '');
|
||||||
setNtfyEnabled(notificationsRes.data.ntfy_enabled ?? true);
|
setNtfyEnabled(notificationsRes.data.ntfy_enabled ?? true);
|
||||||
|
setGotifyUrl(notificationsRes.data.gotify_url || '');
|
||||||
|
setGotifyAppToken(notificationsRes.data.gotify_app_token || '');
|
||||||
|
setGotifyEnabled(notificationsRes.data.gotify_enabled ?? true);
|
||||||
// Populate AI fields with actual values
|
// Populate AI fields with actual values
|
||||||
setAISettings(aiRes.data);
|
setAISettings(aiRes.data);
|
||||||
setAIEnabled(aiRes.data.ai_enabled);
|
setAIEnabled(aiRes.data.ai_enabled);
|
||||||
|
|
@ -363,6 +370,69 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestGotifyConnection = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!gotifyUrl || !gotifyAppToken) {
|
||||||
|
setError('Please enter both the Gotify server URL and app token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsTestingGotify(true);
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.testGotifyConnection(gotifyUrl, gotifyAppToken);
|
||||||
|
if (response.data.success) {
|
||||||
|
setSuccess('Successfully connected to Gotify server!');
|
||||||
|
} else {
|
||||||
|
setError(response.data.error || 'Failed to connect to Gotify');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError('Failed to connect to Gotify. Make sure the server is running.');
|
||||||
|
} finally {
|
||||||
|
setIsTestingGotify(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveGotify = async () => {
|
||||||
|
clearMessages();
|
||||||
|
setIsSavingNotifications(true);
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.updateNotifications({
|
||||||
|
gotify_url: gotifyUrl || null,
|
||||||
|
gotify_app_token: gotifyAppToken || null,
|
||||||
|
});
|
||||||
|
setNotificationSettings(response.data);
|
||||||
|
setGotifyAppToken('');
|
||||||
|
setSuccess('Gotify settings saved successfully');
|
||||||
|
} catch {
|
||||||
|
setError('Failed to save Gotify settings');
|
||||||
|
} finally {
|
||||||
|
setIsSavingNotifications(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestGotify = async () => {
|
||||||
|
clearMessages();
|
||||||
|
setIsTesting('gotify');
|
||||||
|
try {
|
||||||
|
await settingsApi.testGotify();
|
||||||
|
setSuccess('Test notification sent to Gotify!');
|
||||||
|
} catch {
|
||||||
|
setError('Failed to send test notification');
|
||||||
|
} finally {
|
||||||
|
setIsTesting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleGotify = async (enabled: boolean) => {
|
||||||
|
setGotifyEnabled(enabled);
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.updateNotifications({ gotify_enabled: enabled });
|
||||||
|
setNotificationSettings(response.data);
|
||||||
|
} catch {
|
||||||
|
setGotifyEnabled(!enabled);
|
||||||
|
setError('Failed to update Gotify status');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// AI handlers
|
// AI handlers
|
||||||
const handleSaveAI = async () => {
|
const handleSaveAI = async () => {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
|
|
@ -1318,6 +1388,88 @@ export default function Settings() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<div className="settings-section-header">
|
||||||
|
<span className="settings-section-icon">🔔</span>
|
||||||
|
<h2 className="settings-section-title">Gotify Notifications</h2>
|
||||||
|
<span className={`settings-section-status ${notificationSettings?.gotify_url && notificationSettings?.gotify_app_token ? 'configured' : 'not-configured'}`}>
|
||||||
|
{notificationSettings?.gotify_url && notificationSettings?.gotify_app_token ? 'Configured' : 'Not configured'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="settings-section-description">
|
||||||
|
Receive notifications via your self-hosted Gotify server. You'll need to create an application
|
||||||
|
in Gotify to get an app token.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{notificationSettings?.gotify_url && notificationSettings?.gotify_app_token && (
|
||||||
|
<div className="settings-toggle">
|
||||||
|
<div className="settings-toggle-label">
|
||||||
|
<span className="settings-toggle-title">Enable Gotify Notifications</span>
|
||||||
|
<span className="settings-toggle-description">
|
||||||
|
Toggle to enable or disable Gotify alerts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`toggle-switch ${gotifyEnabled ? 'active' : ''}`}
|
||||||
|
onClick={() => handleToggleGotify(!gotifyEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="settings-form-group">
|
||||||
|
<label>Server URL</label>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gotifyUrl}
|
||||||
|
onChange={(e) => setGotifyUrl(e.target.value)}
|
||||||
|
placeholder="https://gotify.example.com"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleTestGotifyConnection}
|
||||||
|
disabled={isTestingGotify || !gotifyUrl || !gotifyAppToken}
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{isTestingGotify ? 'Testing...' : 'Test Connection'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="hint">The URL of your self-hosted Gotify server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-form-group">
|
||||||
|
<label>App Token</label>
|
||||||
|
<PasswordInput
|
||||||
|
value={gotifyAppToken}
|
||||||
|
onChange={(e) => setGotifyAppToken(e.target.value)}
|
||||||
|
placeholder="Enter your app token"
|
||||||
|
/>
|
||||||
|
<p className="hint">
|
||||||
|
Create an application in Gotify (Apps → Create Application) to get a token
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-form-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSaveGotify}
|
||||||
|
disabled={isSavingNotifications}
|
||||||
|
>
|
||||||
|
{isSavingNotifications ? 'Saving...' : 'Save Gotify Settings'}
|
||||||
|
</button>
|
||||||
|
{notificationSettings?.gotify_url && notificationSettings?.gotify_app_token && (
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleTestGotify}
|
||||||
|
disabled={isTesting === 'gotify'}
|
||||||
|
>
|
||||||
|
{isTesting === 'gotify' ? 'Sending...' : 'Send Test'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue