Implement Monitor API v1.1 changes
BREAKING CHANGES: - IP endpoint changed: ip-pool -> servers&for_monitor=1 - BurnVote: removed 'reason' field, 'monitor_id' extracted from token - Evidence format updated with checked_at ISO timestamp NEW FEATURES: - Heartbeat endpoint (POST gateway-sync&action=heartbeat) - Rate limiting with retry logic (429 handling) - 60-second heartbeat timer in main process DEPRECATED: - register() method - token now created via admin panel Files changed: - src/shared/types.ts - Updated PanelIpInfo, BurnVote, added HeartbeatStats - src/main/services/PanelService.ts - Full API v1.1 implementation - src/main/index.ts - Heartbeat timer, checks counter - src/main/preload.ts - panelHeartbeat IPC - src/renderer/types/electron.d.ts - Updated types
This commit is contained in:
parent
c6dbff3fdb
commit
367a1c428e
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -21,7 +21,7 @@ import { NotificationService } from './services/NotificationService'
|
||||||
import { PanelService } from './services/PanelService'
|
import { PanelService } from './services/PanelService'
|
||||||
import { M3u8Parser } from './services/M3u8Parser'
|
import { M3u8Parser } from './services/M3u8Parser'
|
||||||
import { IPC_CHANNELS } from '../shared/types'
|
import { IPC_CHANNELS } from '../shared/types'
|
||||||
import type { AppSettings, Channel, ErrorLogEntry } from '../shared/types'
|
import type { AppSettings, Channel, ErrorLogEntry, HeartbeatStats } from '../shared/types'
|
||||||
|
|
||||||
// App quitting flag
|
// App quitting flag
|
||||||
let isAppQuitting = false
|
let isAppQuitting = false
|
||||||
|
|
@ -34,6 +34,10 @@ let notificationService: NotificationService
|
||||||
let panelService: PanelService
|
let panelService: PanelService
|
||||||
let m3u8Parser: M3u8Parser
|
let m3u8Parser: M3u8Parser
|
||||||
|
|
||||||
|
// Heartbeat timer
|
||||||
|
let heartbeatInterval: NodeJS.Timeout | null = null
|
||||||
|
let checksLastMinute = 0
|
||||||
|
|
||||||
// Window and tray
|
// Window and tray
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let tray: Tray | null = null
|
let tray: Tray | null = null
|
||||||
|
|
@ -166,6 +170,9 @@ function initializeServices() {
|
||||||
streamChecker = new StreamChecker(
|
streamChecker = new StreamChecker(
|
||||||
settings.monitoring,
|
settings.monitoring,
|
||||||
(result) => {
|
(result) => {
|
||||||
|
// Increment checks counter for heartbeat stats
|
||||||
|
incrementChecksCounter()
|
||||||
|
|
||||||
// Send result to renderer
|
// Send result to renderer
|
||||||
mainWindow?.webContents.send(IPC_CHANNELS.MONITORING_UPDATE, result)
|
mainWindow?.webContents.send(IPC_CHANNELS.MONITORING_UPDATE, result)
|
||||||
|
|
||||||
|
|
@ -201,6 +208,67 @@ function initializeServices() {
|
||||||
console.log('[App] Portable mode:', isPortableMode())
|
console.log('[App] Portable mode:', isPortableMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start heartbeat timer - sends heartbeat every 60 seconds
|
||||||
|
* API v1.1: New heartbeat endpoint for monitor liveness
|
||||||
|
*/
|
||||||
|
function startHeartbeat(): void {
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings()
|
||||||
|
if (!settings.panel.enabled) {
|
||||||
|
console.log('[Heartbeat] Panel not enabled, skipping heartbeat')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send heartbeat every 60 seconds
|
||||||
|
heartbeatInterval = setInterval(async () => {
|
||||||
|
if (!panelService.isEnabled()) return
|
||||||
|
|
||||||
|
const channels = getChannels()
|
||||||
|
const monitoredChannels = channels.filter(c => c.isMonitored)
|
||||||
|
|
||||||
|
const stats: HeartbeatStats = {
|
||||||
|
active_checks: monitoredChannels.length,
|
||||||
|
checks_last_minute: checksLastMinute,
|
||||||
|
cpu_usage_percent: process.cpuUsage().user / 1000000, // Convert to percent
|
||||||
|
memory_usage_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await panelService.sendHeartbeat(stats)
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[Heartbeat] Sent successfully')
|
||||||
|
} else {
|
||||||
|
console.warn('[Heartbeat] Failed:', result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset checks counter
|
||||||
|
checksLastMinute = 0
|
||||||
|
}, 60000) // 60 seconds
|
||||||
|
|
||||||
|
console.log('[Heartbeat] Timer started (60s interval)')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop heartbeat timer
|
||||||
|
*/
|
||||||
|
function stopHeartbeat(): void {
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval)
|
||||||
|
heartbeatInterval = null
|
||||||
|
console.log('[Heartbeat] Timer stopped')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment checks counter for heartbeat stats
|
||||||
|
*/
|
||||||
|
function incrementChecksCounter(): void {
|
||||||
|
checksLastMinute++
|
||||||
|
}
|
||||||
|
|
||||||
function setupIpcHandlers() {
|
function setupIpcHandlers() {
|
||||||
// Window controls
|
// Window controls
|
||||||
ipcMain.on('window:minimize', () => mainWindow?.minimize())
|
ipcMain.on('window:minimize', () => mainWindow?.minimize())
|
||||||
|
|
@ -214,7 +282,7 @@ function setupIpcHandlers() {
|
||||||
ipcMain.on('window:close', () => {
|
ipcMain.on('window:close', () => {
|
||||||
// Çarpı butonuna basınca uygulamayı tamamen kapat
|
// Çarpı butonuna basınca uygulamayı tamamen kapat
|
||||||
isAppQuitting = true
|
isAppQuitting = true
|
||||||
mainWindow?.close()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
// IP Service
|
// IP Service
|
||||||
|
|
@ -296,6 +364,11 @@ function setupIpcHandlers() {
|
||||||
return await panelService.sendBurnVote(vote)
|
return await panelService.sendBurnVote(vote)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Panel Heartbeat - API v1.1
|
||||||
|
ipcMain.handle(IPC_CHANNELS.PANEL_HEARTBEAT, async (_, stats: HeartbeatStats) => {
|
||||||
|
return await panelService.sendHeartbeat(stats)
|
||||||
|
})
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_SETTINGS, async () => {
|
ipcMain.handle(IPC_CHANNELS.GET_SETTINGS, async () => {
|
||||||
return getSettings()
|
return getSettings()
|
||||||
|
|
@ -337,6 +410,9 @@ app.whenReady().then(() => {
|
||||||
createWindow()
|
createWindow()
|
||||||
createTray()
|
createTray()
|
||||||
setupIpcHandlers()
|
setupIpcHandlers()
|
||||||
|
|
||||||
|
// Start heartbeat timer - API v1.1
|
||||||
|
startHeartbeat()
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
|
@ -353,5 +429,6 @@ app.on('window-all-closed', () => {
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
isAppQuitting = true
|
isAppQuitting = true
|
||||||
|
stopHeartbeat()
|
||||||
streamChecker?.stop()
|
streamChecker?.stop()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
import { IPC_CHANNELS } from '../shared/types'
|
import { IPC_CHANNELS } from '../shared/types'
|
||||||
import type { Channel, AppSettings, BurnVote, NotificationType } from '../shared/types'
|
import type { Channel, AppSettings, BurnVote, NotificationType, HeartbeatStats } from '../shared/types'
|
||||||
|
|
||||||
// Expose protected methods to renderer
|
// Expose protected methods to renderer
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
|
@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||||
panelRegister: (config: unknown) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_REGISTER, config),
|
panelRegister: (config: unknown) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_REGISTER, config),
|
||||||
panelGetIps: () => ipcRenderer.invoke(IPC_CHANNELS.PANEL_GET_IPS),
|
panelGetIps: () => ipcRenderer.invoke(IPC_CHANNELS.PANEL_GET_IPS),
|
||||||
panelSendVote: (vote: BurnVote) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_SEND_VOTE, vote),
|
panelSendVote: (vote: BurnVote) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_SEND_VOTE, vote),
|
||||||
|
panelHeartbeat: (stats: HeartbeatStats) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_HEARTBEAT, stats),
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS),
|
getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,28 @@
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance, AxiosError } from 'axios'
|
||||||
import type { PanelConfig, PanelIpPool, BurnVote } from '../../shared/types'
|
import type {
|
||||||
|
PanelConfig,
|
||||||
|
PanelIpPool,
|
||||||
|
BurnVote,
|
||||||
|
HeartbeatStats,
|
||||||
|
HeartbeatResponse,
|
||||||
|
PanelIpInfo
|
||||||
|
} from '../../shared/types'
|
||||||
|
|
||||||
|
// Rate limiting state
|
||||||
|
interface RateLimitState {
|
||||||
|
isLimited: boolean
|
||||||
|
retryAfter: number
|
||||||
|
lastLimitTime: number
|
||||||
|
}
|
||||||
|
|
||||||
export class PanelService {
|
export class PanelService {
|
||||||
private config: PanelConfig
|
private config: PanelConfig
|
||||||
private client: AxiosInstance | null = null
|
private client: AxiosInstance | null = null
|
||||||
|
private rateLimitState: RateLimitState = {
|
||||||
|
isLimited: false,
|
||||||
|
retryAfter: 0,
|
||||||
|
lastLimitTime: 0
|
||||||
|
}
|
||||||
|
|
||||||
constructor(config: PanelConfig) {
|
constructor(config: PanelConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
@ -26,29 +45,50 @@ export class PanelService {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
// API v1.1: Token is now 64-char hex from admin panel
|
||||||
...(this.config.authToken && {
|
...(this.config.authToken && {
|
||||||
'Authorization': `Bearer ${this.config.authToken}`
|
'Authorization': `Bearer ${this.config.authToken}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add response interceptor for rate limiting
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10)
|
||||||
|
this.rateLimitState = {
|
||||||
|
isLimited: true,
|
||||||
|
retryAfter,
|
||||||
|
lastLimitTime: Date.now()
|
||||||
|
}
|
||||||
|
console.warn(`[PanelService] Rate limited. Retry after ${retryAfter}s`)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register monitor with panel
|
* @deprecated API v1.1: Token is now created via admin panel
|
||||||
* Based on MONITOR_API_GUIDE.md
|
* This method is kept for backwards compatibility but will log a warning
|
||||||
*/
|
*/
|
||||||
async register(): Promise<{ success: boolean; authToken?: string; error?: string }> {
|
async register(): Promise<{ success: boolean; authToken?: string; error?: string }> {
|
||||||
|
console.warn('[PanelService] register() is deprecated in API v1.1. Token should be created via admin panel.')
|
||||||
|
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
return { success: false, error: 'Panel not configured' }
|
return { success: false, error: 'Panel not configured' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const os = await import('os')
|
||||||
const response = await this.client.post('/api.php?endpoint=servers', {
|
const response = await this.client.post('/api.php?endpoint=servers', {
|
||||||
server_id: this.config.monitorId,
|
server_id: this.config.monitorId,
|
||||||
server_type: 'monitor',
|
server_type: 'monitor',
|
||||||
hostname: require('os').hostname(),
|
hostname: os.hostname(),
|
||||||
ip_address: await this.getLocalIp(),
|
ip_address: await this.getLocalIp(),
|
||||||
country_code: 'TR' // Could be detected dynamically
|
country_code: 'TR'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
|
|
@ -66,104 +106,208 @@ export class PanelService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get IP pool from panel
|
* Get IP pool from panel
|
||||||
* Based on MONITOR_API_GUIDE.md - Option A: REST API (Polling)
|
* API v1.1: Changed endpoint from ip-pool to servers&for_monitor=1
|
||||||
*/
|
*/
|
||||||
async getIpPool(): Promise<PanelIpPool> {
|
async getIpPool(): Promise<PanelIpPool> {
|
||||||
if (!this.client || !this.config.enabled) {
|
if (!this.client || !this.config.enabled) {
|
||||||
return { active: [], honeypot: [] }
|
return { ips: [], timestamp: Date.now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rate limit
|
||||||
|
if (this.isRateLimited()) {
|
||||||
|
console.warn('[PanelService] Rate limited, skipping getIpPool')
|
||||||
|
return { ips: [], timestamp: Date.now() }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// API v1.1: New endpoint
|
||||||
const response = await this.client.get('/api.php', {
|
const response = await this.client.get('/api.php', {
|
||||||
params: {
|
params: {
|
||||||
endpoint: 'ip-pool',
|
endpoint: 'servers',
|
||||||
for_monitor: true
|
for_monitor: '1'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.success) {
|
// API v1.1: New response format
|
||||||
return response.data.ips || { active: [], honeypot: [] }
|
if (response.data.ips) {
|
||||||
|
return {
|
||||||
|
ips: response.data.ips as PanelIpInfo[],
|
||||||
|
timestamp: response.data.timestamp || Date.now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Failed to get IP pool:', response.data.message)
|
// Legacy format support
|
||||||
return { active: [], honeypot: [] }
|
if (response.data.success && response.data.ips) {
|
||||||
|
const legacyIps = response.data.ips
|
||||||
|
return {
|
||||||
|
ips: [...(legacyIps.active || []), ...(legacyIps.honeypot || [])],
|
||||||
|
timestamp: response.data.timestamp || Date.now(),
|
||||||
|
active: legacyIps.active,
|
||||||
|
honeypot: legacyIps.honeypot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[PanelService] Failed to get IP pool:', response.data.message)
|
||||||
|
return { ips: [], timestamp: Date.now() }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch IP pool:', error)
|
if ((error as AxiosError).response?.status !== 429) {
|
||||||
return { active: [], honeypot: [] }
|
console.error('[PanelService] Failed to fetch IP pool:', error)
|
||||||
|
}
|
||||||
|
return { ips: [], timestamp: Date.now() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send burn vote
|
* Send burn vote
|
||||||
* Based on MONITOR_API_GUIDE.md - Section 2: Burn Bildirimi
|
* API v1.1: monitor_id removed from payload (extracted from Bearer token)
|
||||||
|
* API v1.1: reason field removed, evidence format changed
|
||||||
*/
|
*/
|
||||||
async sendBurnVote(vote: BurnVote): Promise<{ success: boolean; error?: string }> {
|
async sendBurnVote(vote: BurnVote): Promise<{ success: boolean; error?: string; currentVotes?: object }> {
|
||||||
if (!this.client || !this.config.enabled) {
|
if (!this.client || !this.config.enabled) {
|
||||||
return { success: false, error: 'Panel not enabled' }
|
return { success: false, error: 'Panel not enabled' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check rate limit
|
||||||
|
if (this.isRateLimited()) {
|
||||||
|
console.warn('[PanelService] Rate limited, queuing burn vote')
|
||||||
|
return { success: false, error: 'Rate limited, try again later' }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// API v1.1: New payload format - no monitor_id, no reason
|
||||||
const response = await this.client.post('/api.php?endpoint=burn-vote', {
|
const response = await this.client.post('/api.php?endpoint=burn-vote', {
|
||||||
ip_address: vote.ip_address,
|
ip_address: vote.ip_address,
|
||||||
vote: vote.vote,
|
vote: vote.vote,
|
||||||
reason: vote.reason,
|
evidence: vote.evidence
|
||||||
evidence: {
|
|
||||||
...vote.evidence,
|
|
||||||
monitor_id: this.config.monitorId,
|
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
console.log('Burn vote submitted:', {
|
console.log('[PanelService] Burn vote submitted:', {
|
||||||
ip: vote.ip_address,
|
ip: vote.ip_address,
|
||||||
vote: vote.vote,
|
vote: vote.vote,
|
||||||
current_votes: response.data.current_votes
|
current_votes: response.data.current_votes
|
||||||
})
|
})
|
||||||
return { success: true }
|
return {
|
||||||
|
success: true,
|
||||||
|
currentVotes: response.data.current_votes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: response.data.message }
|
return { success: false, error: response.data.message }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError
|
||||||
|
|
||||||
|
// Handle 403 - wrong server type
|
||||||
|
if (axiosError.response?.status === 403) {
|
||||||
|
const errorData = axiosError.response.data as { message?: string }
|
||||||
|
console.error('[PanelService] Forbidden:', errorData.message)
|
||||||
|
return { success: false, error: errorData.message || 'Forbidden: Server type not allowed' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on rate limit
|
||||||
|
if (axiosError.response?.status === 429) {
|
||||||
|
const retryAfter = parseInt(axiosError.response.headers['retry-after'] || '60', 10)
|
||||||
|
console.warn(`[PanelService] Rate limited on burn vote. Waiting ${retryAfter}s...`)
|
||||||
|
|
||||||
|
// Wait and retry once
|
||||||
|
await this.sleep(retryAfter * 1000)
|
||||||
|
return this.sendBurnVote(vote)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: false, error: (error as Error).message }
|
return { success: false, error: (error as Error).message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report IP health status
|
* Send heartbeat to panel
|
||||||
|
* API v1.1: New endpoint for monitor liveness
|
||||||
|
*/
|
||||||
|
async sendHeartbeat(stats: HeartbeatStats): Promise<HeartbeatResponse> {
|
||||||
|
if (!this.client || !this.config.enabled) {
|
||||||
|
return { success: false, message: 'Panel not enabled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rate limit
|
||||||
|
if (this.isRateLimited()) {
|
||||||
|
return { success: false, message: 'Rate limited' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.post(
|
||||||
|
'/api.php?endpoint=gateway-sync&action=heartbeat',
|
||||||
|
{ data: stats }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('[PanelService] Heartbeat sent successfully')
|
||||||
|
return { success: true, message: response.data.message || 'Heartbeat received' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: response.data.message || 'Heartbeat failed' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PanelService] Heartbeat failed:', error)
|
||||||
|
return { success: false, message: (error as Error).message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report IP health status - updated for API v1.1
|
||||||
*/
|
*/
|
||||||
async reportHealth(
|
async reportHealth(
|
||||||
ipAddress: string,
|
ipAddress: string,
|
||||||
isWorking: boolean,
|
isWorking: boolean,
|
||||||
checkResult: {
|
checkResult: {
|
||||||
checkType: string
|
checkType: 'http_health' | 'tcp_connect' | 'ssl_verify' | 'dns_lookup'
|
||||||
port?: number
|
responseCode?: number
|
||||||
latencyMs?: number
|
latencyMs?: number
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// API v1.1: New evidence format with checked_at ISO timestamp
|
||||||
const vote: BurnVote = {
|
const vote: BurnVote = {
|
||||||
ip_address: ipAddress,
|
ip_address: ipAddress,
|
||||||
vote: isWorking ? 'ok' : 'burn',
|
vote: isWorking ? 'ok' : 'burn',
|
||||||
reason: isWorking ? 'IP working normally' : checkResult.error || 'Check failed',
|
|
||||||
evidence: {
|
evidence: {
|
||||||
check_type: checkResult.checkType,
|
check_type: checkResult.checkType,
|
||||||
port: checkResult.port,
|
response_code: checkResult.responseCode || 0,
|
||||||
response_time_ms: checkResult.latencyMs,
|
latency_ms: checkResult.latencyMs || 0,
|
||||||
error_message: checkResult.error,
|
error_message: checkResult.error || null,
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
checked_at: new Date().toISOString()
|
||||||
monitor_id: this.config.monitorId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sendBurnVote(vote)
|
await this.sendBurnVote(vote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if currently rate limited
|
||||||
|
*/
|
||||||
|
private isRateLimited(): boolean {
|
||||||
|
if (!this.rateLimitState.isLimited) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = (Date.now() - this.rateLimitState.lastLimitTime) / 1000
|
||||||
|
if (elapsed >= this.rateLimitState.retryAfter) {
|
||||||
|
this.rateLimitState.isLimited = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep utility for retry logic
|
||||||
|
*/
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get local IP address
|
* Get local IP address
|
||||||
*/
|
*/
|
||||||
private async getLocalIp(): Promise<string> {
|
private async getLocalIp(): Promise<string> {
|
||||||
const os = require('os')
|
const os = await import('os')
|
||||||
const interfaces = os.networkInterfaces()
|
const interfaces = os.networkInterfaces()
|
||||||
|
|
||||||
for (const name of Object.keys(interfaces)) {
|
for (const name of Object.keys(interfaces)) {
|
||||||
|
|
@ -183,5 +327,21 @@ export class PanelService {
|
||||||
isEnabled(): boolean {
|
isEnabled(): boolean {
|
||||||
return this.config.enabled && !!this.config.apiUrl && !!this.config.authToken
|
return this.config.enabled && !!this.config.apiUrl && !!this.config.authToken
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate limit status for UI display
|
||||||
|
*/
|
||||||
|
getRateLimitStatus(): { isLimited: boolean; secondsRemaining: number } {
|
||||||
|
if (!this.rateLimitState.isLimited) {
|
||||||
|
return { isLimited: false, secondsRemaining: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = (Date.now() - this.rateLimitState.lastLimitTime) / 1000
|
||||||
|
const remaining = Math.max(0, this.rateLimitState.retryAfter - elapsed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLimited: remaining > 0,
|
||||||
|
secondsRemaining: Math.ceil(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export interface ElectronAPI {
|
||||||
panelRegister: (config: import('@shared/types').PanelConfig) => Promise<{ success: boolean; authToken?: string }>
|
panelRegister: (config: import('@shared/types').PanelConfig) => Promise<{ success: boolean; authToken?: string }>
|
||||||
panelGetIps: () => Promise<import('@shared/types').PanelIpPool>
|
panelGetIps: () => Promise<import('@shared/types').PanelIpPool>
|
||||||
panelSendVote: (vote: import('@shared/types').BurnVote) => Promise<{ success: boolean }>
|
panelSendVote: (vote: import('@shared/types').BurnVote) => Promise<{ success: boolean }>
|
||||||
|
panelHeartbeat: (stats: import('@shared/types').HeartbeatStats) => Promise<import('@shared/types').HeartbeatResponse>
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
getSettings: () => Promise<import('@shared/types').AppSettings>
|
getSettings: () => Promise<import('@shared/types').AppSettings>
|
||||||
|
|
|
||||||
|
|
@ -71,37 +71,62 @@ export interface NotificationSettings {
|
||||||
notifyOnConnReset: boolean;
|
notifyOnConnReset: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel API Types (from MONITOR_API_GUIDE)
|
// Panel API Types (from MONITOR_API_GUIDE v1.1)
|
||||||
export interface PanelConfig {
|
export interface PanelConfig {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
authToken: string;
|
authToken: string; // Now 64-char hex token from admin panel
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated for API v1.1 - new response format from servers endpoint
|
||||||
export interface PanelIpInfo {
|
export interface PanelIpInfo {
|
||||||
ip_address: string;
|
ip_address: string;
|
||||||
server_id: string;
|
server_id: string;
|
||||||
check_priority: 'high' | 'normal' | 'low';
|
server_type: 'publisher' | 'monitor' | 'gateway';
|
||||||
|
status: 'active' | 'inactive' | 'burned';
|
||||||
|
active_users?: number;
|
||||||
|
// Legacy field for backwards compatibility
|
||||||
|
check_priority?: 'high' | 'normal' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated response format for API v1.1
|
||||||
export interface PanelIpPool {
|
export interface PanelIpPool {
|
||||||
active: PanelIpInfo[];
|
ips: PanelIpInfo[];
|
||||||
honeypot: PanelIpInfo[];
|
timestamp: number;
|
||||||
|
// Legacy fields for backwards compatibility
|
||||||
|
active?: PanelIpInfo[];
|
||||||
|
honeypot?: PanelIpInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated for API v1.1 - monitor_id removed (extracted from Bearer token)
|
||||||
export interface BurnVote {
|
export interface BurnVote {
|
||||||
ip_address: string;
|
ip_address: string;
|
||||||
vote: 'burn' | 'ok' | 'abstain';
|
vote: 'burn' | 'ok' | 'abstain';
|
||||||
reason: string;
|
evidence: BurnVoteEvidence;
|
||||||
evidence: {
|
}
|
||||||
check_type: string;
|
|
||||||
port?: number;
|
// New evidence format for API v1.1
|
||||||
response_time_ms?: number;
|
export interface BurnVoteEvidence {
|
||||||
error_message?: string;
|
check_type: 'http_health' | 'tcp_connect' | 'ssl_verify' | 'dns_lookup';
|
||||||
timestamp: number;
|
response_code: number;
|
||||||
monitor_id: string;
|
latency_ms: number;
|
||||||
};
|
error_message: string | null;
|
||||||
|
checked_at: string; // ISO 8601 format
|
||||||
|
}
|
||||||
|
|
||||||
|
// New for API v1.1 - Heartbeat stats
|
||||||
|
export interface HeartbeatStats {
|
||||||
|
active_checks: number;
|
||||||
|
checks_last_minute: number;
|
||||||
|
cpu_usage_percent?: number;
|
||||||
|
memory_usage_mb?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat response
|
||||||
|
export interface HeartbeatResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App Settings
|
// App Settings
|
||||||
|
|
@ -147,9 +172,10 @@ export const IPC_CHANNELS = {
|
||||||
TEST_NOTIFICATION: 'notify:test',
|
TEST_NOTIFICATION: 'notify:test',
|
||||||
|
|
||||||
// Panel API
|
// Panel API
|
||||||
PANEL_REGISTER: 'panel:register',
|
PANEL_REGISTER: 'panel:register', // Deprecated in v1.1
|
||||||
PANEL_GET_IPS: 'panel:get-ips',
|
PANEL_GET_IPS: 'panel:get-ips',
|
||||||
PANEL_SEND_VOTE: 'panel:send-vote',
|
PANEL_SEND_VOTE: 'panel:send-vote',
|
||||||
|
PANEL_HEARTBEAT: 'panel:heartbeat',
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
GET_SETTINGS: 'settings:get',
|
GET_SETTINGS: 'settings:get',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue