diff --git a/src/app/components/Settings/SettingsButtons.tsx b/src/app/components/Settings/SettingsButtons.tsx index 3fb0058..58eb14e 100644 --- a/src/app/components/Settings/SettingsButtons.tsx +++ b/src/app/components/Settings/SettingsButtons.tsx @@ -1,21 +1,21 @@ import { - ColorContext, - ColorSelectionType, - SessionContext, + ColorContext, + ColorSelectionType, + SessionContext, } from "@/app/context/Context"; import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Tooltip, - Typography, - TextField, - Box, - FormControlLabel, - Checkbox, - Divider, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Tooltip, + Typography, + TextField, + Box, + FormControlLabel, + Checkbox, + Divider, } from "@mui/material"; import { ColorResult, CompactPicker } from "react-color"; import React, { useState, useContext } from "react"; @@ -78,6 +78,11 @@ export const SettingsButton = () => { } }; + const handleRefreshStats = () => { + // Trigger a manual refresh of character data + window.location.reload(); + }; + return ( <> @@ -121,6 +126,18 @@ export const SettingsButton = () => { /> + + Data Refresh + + + Webhook Notifications @@ -162,19 +179,32 @@ export const SettingsButton = () => { helperText="Alert when storage reaches this percentage (0-100)" /> - - - )} - + + + + + )} + diff --git a/src/app/page.tsx b/src/app/page.tsx index aafd46b..a1215fb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -51,11 +51,19 @@ const Home = () => { const [balanceThreshold, setBalanceThreshold] = useState(1000); const [showProductIcons, setShowProductIcons] = useState(false); const [extractionTimeMode, setExtractionTimeMode] = useState(false); - const [webhookConfig, setWebhookConfig] = useState({ - enabled: true, // Enable by default for testing - expiryThreshold: 'P12H', - storageWarningThreshold: 85, - storageCriticalThreshold: 100 + const [webhookConfig, setWebhookConfig] = useState(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem("webhookConfig"); + if (stored) { + return JSON.parse(stored); + } + } + return { + enabled: false, + expiryThreshold: 'P12H', + storageWarningThreshold: 85, + storageCriticalThreshold: 100 + }; }); const [webhookServerEnabled, setWebhookServerEnabled] = useState(false); @@ -333,6 +341,7 @@ const Home = () => { } }, [webhookConfig]); + useEffect(() => { console.log('🔧 Initializing app...'); fetch("api/env") diff --git a/src/utils/notificationService.ts b/src/utils/notificationService.ts new file mode 100644 index 0000000..3d2af1d --- /dev/null +++ b/src/utils/notificationService.ts @@ -0,0 +1,102 @@ +import { DateTime } from "luxon"; +import { AccessToken, WebhookConfig } from "@/types"; +import { planetCalculations } from "@/planets"; + +export class NotificationService { + private notificationState = new Map(); + private lastWebhookTime = 0; + private readonly WEBHOOK_COOLDOWN = 5000; // 5 seconds between webhooks + + public checkAndNotify = async ( + characters: AccessToken[], + webhookConfig: WebhookConfig + ): Promise => { + if (!webhookConfig.enabled) return; + + const notifications: string[] = []; + const now = DateTime.now(); + + for (const character of characters) { + for (const planet of character.planets) { + const planetDetails = planetCalculations(planet); + + // Check extractors for expiry warnings + for (const extractor of planetDetails.extractors) { + if (extractor.expiry_time) { + const expiryTime = DateTime.fromISO(extractor.expiry_time); + const identifier = `${character.character.name}-${planet.planet_id}-${extractor.pin_id}`; + + // Check if already expired + if (expiryTime <= now) { + if (!this.notificationState.get(`${identifier}-expired`)) { + notifications.push(`🚨 EXTRACTOR EXPIRED - ${character.character.name} Planet ${planet.planet_id}`); + this.notificationState.set(`${identifier}-expired`, true); + } + } else { + // Check if expiring soon (within the warning threshold) + const warningThreshold = this.parseDuration(webhookConfig.expiryThreshold || "P12H"); + const warningTime = now.plus(warningThreshold); + + if (expiryTime <= warningTime && expiryTime > now) { + if (!this.notificationState.get(`${identifier}-warning`)) { + const hoursLeft = Math.round(expiryTime.diff(now, "hours").hours); + notifications.push(`⚠️ EXTRACTOR EXPIRING SOON - ${character.character.name} Planet ${planet.planet_id} (${hoursLeft}h left)`); + this.notificationState.set(`${identifier}-warning`, true); + } + } + } + } + } + + // Check storage capacity + for (const storage of planetDetails.storageInfo) { + const identifier = `${character.character.name}-${planet.planet_id}-${storage.type_id}`; + + if (storage.fillRate >= (webhookConfig.storageCriticalThreshold || 100)) { + if (!this.notificationState.get(`${identifier}-critical`)) { + notifications.push(`🚨 STORAGE CRITICAL - ${character.character.name} Planet ${planet.planet_id} (${storage.fillRate.toFixed(1)}%)`); + this.notificationState.set(`${identifier}-critical`, true); + } + } else if (storage.fillRate >= (webhookConfig.storageWarningThreshold || 85)) { + if (!this.notificationState.get(`${identifier}-warning`)) { + notifications.push(`⚠️ STORAGE WARNING - ${character.character.name} Planet ${planet.planet_id} (${storage.fillRate.toFixed(1)}%)`); + this.notificationState.set(`${identifier}-warning`, true); + } + } + } + } + } + + if (notifications.length > 0) { + await this.sendWebhook(notifications.join("\n")); + } + }; + + private parseDuration(duration: string): any { + // Parse ISO 8601 duration (e.g., "P12H", "P1D", "PT2H30M") + try { + return DateTime.fromISO(`2000-01-01T00:00:00Z`).plus({ [duration.slice(-1) === 'H' ? 'hours' : duration.slice(-1) === 'D' ? 'days' : 'minutes' ]: parseInt(duration.slice(1, -1)) }).diff(DateTime.fromISO(`2000-01-01T00:00:00Z`)); + } catch { + // Default to 12 hours if parsing fails + return { hours: 12 }; + } + } + + private sendWebhook = async (content: string): Promise => { + try { + const response = await fetch("/api/webhook", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); + + if (!response.ok) { + throw new Error(`Webhook failed: ${response.status}`); + } + } catch (error) { + console.error("Webhook error:", error); + } + }; +} + +export const notificationService = new NotificationService();