mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-19 21:23:56 +00:00
refactor: remove legacy scheduling and analytics code
- Deleted the scheduler module and its associated functions, transitioning to OS-level scheduling. - Removed the Analytics module and its related interfaces, retaining only a placeholder for backward compatibility. - Updated ConfigValidator to warn about legacy schedule and analytics configurations. - Cleaned up StartupValidator to remove diagnostics and schedule validation logic. - Adjusted Load.ts to handle legacy flags for diagnostics and analytics. - Removed unused diagnostics capturing functionality.
This commit is contained in:
@@ -1,264 +1,3 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export interface DailyMetrics {
|
||||
date: string // YYYY-MM-DD
|
||||
email: string
|
||||
pointsEarned: number
|
||||
pointsInitial: number
|
||||
pointsEnd: number
|
||||
desktopPoints: number
|
||||
mobilePoints: number
|
||||
executionTimeMs: number
|
||||
successRate: number // 0-1
|
||||
errorsCount: number
|
||||
banned: boolean
|
||||
riskScore?: number
|
||||
}
|
||||
|
||||
export interface AccountHistory {
|
||||
email: string
|
||||
totalRuns: number
|
||||
totalPointsEarned: number
|
||||
avgPointsPerDay: number
|
||||
avgExecutionTime: number
|
||||
successRate: number
|
||||
lastRunDate: string
|
||||
banHistory: Array<{ date: string; reason: string }>
|
||||
riskTrend: number[] // last N risk scores
|
||||
}
|
||||
|
||||
export interface AnalyticsSummary {
|
||||
period: string // e.g., 'last-7-days', 'last-30-days', 'all-time'
|
||||
accounts: AccountHistory[]
|
||||
globalStats: {
|
||||
totalPoints: number
|
||||
avgSuccessRate: number
|
||||
mostProductiveAccount: string
|
||||
mostRiskyAccount: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics tracks performance metrics, point collection trends, and account health.
|
||||
* Stores data in JSON files for lightweight persistence and easy analysis.
|
||||
*/
|
||||
export class Analytics {
|
||||
private dataDir: string
|
||||
|
||||
constructor(baseDir: string = 'analytics') {
|
||||
this.dataDir = path.join(process.cwd(), baseDir)
|
||||
if (!fs.existsSync(this.dataDir)) {
|
||||
fs.mkdirSync(this.dataDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record metrics for a completed account run
|
||||
*/
|
||||
recordRun(metrics: DailyMetrics): void {
|
||||
const date = metrics.date
|
||||
const email = this.sanitizeEmail(metrics.email)
|
||||
const fileName = `${email}_${date}.json`
|
||||
const filePath = path.join(this.dataDir, fileName)
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(metrics, null, 2), 'utf-8')
|
||||
} catch (error) {
|
||||
console.error(`Failed to save metrics for ${metrics.email}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get history for a specific account
|
||||
*/
|
||||
getAccountHistory(email: string, days: number = 30): AccountHistory {
|
||||
const sanitized = this.sanitizeEmail(email)
|
||||
const files = this.getAccountFiles(sanitized, days)
|
||||
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
email,
|
||||
totalRuns: 0,
|
||||
totalPointsEarned: 0,
|
||||
avgPointsPerDay: 0,
|
||||
avgExecutionTime: 0,
|
||||
successRate: 1.0,
|
||||
lastRunDate: 'never',
|
||||
banHistory: [],
|
||||
riskTrend: []
|
||||
}
|
||||
}
|
||||
|
||||
let totalPoints = 0
|
||||
let totalTime = 0
|
||||
let successCount = 0
|
||||
const banHistory: Array<{ date: string; reason: string }> = []
|
||||
const riskScores: number[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.dataDir, file)
|
||||
try {
|
||||
const data: DailyMetrics = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
||||
totalPoints += data.pointsEarned
|
||||
totalTime += data.executionTimeMs
|
||||
if (data.successRate > 0.5) successCount++
|
||||
if (data.banned) {
|
||||
banHistory.push({ date: data.date, reason: 'detected' })
|
||||
}
|
||||
if (typeof data.riskScore === 'number') {
|
||||
riskScores.push(data.riskScore)
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const totalRuns = files.length
|
||||
const lastFile = files[files.length - 1]
|
||||
const lastRunDate = lastFile ? lastFile.split('_')[1]?.replace('.json', '') || 'unknown' : 'unknown'
|
||||
|
||||
return {
|
||||
email,
|
||||
totalRuns,
|
||||
totalPointsEarned: totalPoints,
|
||||
avgPointsPerDay: Math.round(totalPoints / Math.max(1, totalRuns)),
|
||||
avgExecutionTime: Math.round(totalTime / Math.max(1, totalRuns)),
|
||||
successRate: successCount / Math.max(1, totalRuns),
|
||||
lastRunDate,
|
||||
banHistory,
|
||||
riskTrend: riskScores.slice(-10) // last 10 risk scores
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a summary report for all accounts
|
||||
*/
|
||||
generateSummary(days: number = 30): AnalyticsSummary {
|
||||
const accountEmails = this.getAllAccounts()
|
||||
const accounts: AccountHistory[] = []
|
||||
|
||||
for (const email of accountEmails) {
|
||||
accounts.push(this.getAccountHistory(email, days))
|
||||
}
|
||||
|
||||
const totalPoints = accounts.reduce((sum, a) => sum + a.totalPointsEarned, 0)
|
||||
const avgSuccess = accounts.reduce((sum, a) => sum + a.successRate, 0) / Math.max(1, accounts.length)
|
||||
|
||||
let mostProductive = ''
|
||||
let maxPoints = 0
|
||||
let mostRisky = ''
|
||||
let maxRisk = 0
|
||||
|
||||
for (const acc of accounts) {
|
||||
if (acc.totalPointsEarned > maxPoints) {
|
||||
maxPoints = acc.totalPointsEarned
|
||||
mostProductive = acc.email
|
||||
}
|
||||
const avgRisk = acc.riskTrend.reduce((s, r) => s + r, 0) / Math.max(1, acc.riskTrend.length)
|
||||
if (avgRisk > maxRisk) {
|
||||
maxRisk = avgRisk
|
||||
mostRisky = acc.email
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
period: `last-${days}-days`,
|
||||
accounts,
|
||||
globalStats: {
|
||||
totalPoints,
|
||||
avgSuccessRate: Number(avgSuccess.toFixed(2)),
|
||||
mostProductiveAccount: mostProductive || 'none',
|
||||
mostRiskyAccount: mostRisky || 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export summary as markdown table (for human readability)
|
||||
*/
|
||||
exportMarkdown(days: number = 30): string {
|
||||
const summary = this.generateSummary(days)
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push(`# Analytics Summary (${summary.period})`)
|
||||
lines.push('')
|
||||
lines.push('## Global Stats')
|
||||
lines.push(`- Total Points: ${summary.globalStats.totalPoints}`)
|
||||
lines.push(`- Avg Success Rate: ${(summary.globalStats.avgSuccessRate * 100).toFixed(1)}%`)
|
||||
lines.push(`- Most Productive: ${summary.globalStats.mostProductiveAccount}`)
|
||||
lines.push(`- Most Risky: ${summary.globalStats.mostRiskyAccount}`)
|
||||
lines.push('')
|
||||
lines.push('## Per-Account Breakdown')
|
||||
lines.push('')
|
||||
lines.push('| Account | Runs | Total Points | Avg/Day | Success Rate | Last Run | Bans |')
|
||||
lines.push('|---------|------|--------------|---------|--------------|----------|------|')
|
||||
|
||||
for (const acc of summary.accounts) {
|
||||
const successPct = (acc.successRate * 100).toFixed(0)
|
||||
const banCount = acc.banHistory.length
|
||||
lines.push(
|
||||
`| ${acc.email} | ${acc.totalRuns} | ${acc.totalPointsEarned} | ${acc.avgPointsPerDay} | ${successPct}% | ${acc.lastRunDate} | ${banCount} |`
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old analytics files (retention policy)
|
||||
*/
|
||||
cleanup(retentionDays: number): void {
|
||||
const files = fs.readdirSync(this.dataDir)
|
||||
const cutoff = Date.now() - (retentionDays * 24 * 60 * 60 * 1000)
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.json')) continue
|
||||
const filePath = path.join(this.dataDir, file)
|
||||
try {
|
||||
const stats = fs.statSync(filePath)
|
||||
if (stats.mtimeMs < cutoff) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeEmail(email: string): string {
|
||||
return email.replace(/[^a-zA-Z0-9@._-]/g, '_')
|
||||
}
|
||||
|
||||
private getAccountFiles(sanitizedEmail: string, days: number): string[] {
|
||||
const files = fs.readdirSync(this.dataDir)
|
||||
const cutoffDate = new Date()
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days)
|
||||
|
||||
return files
|
||||
.filter((f: string) => f.startsWith(sanitizedEmail) && f.endsWith('.json'))
|
||||
.filter((f: string) => {
|
||||
const datePart = f.split('_')[1]?.replace('.json', '')
|
||||
if (!datePart) return false
|
||||
const fileDate = new Date(datePart)
|
||||
return fileDate >= cutoffDate
|
||||
})
|
||||
.sort()
|
||||
}
|
||||
|
||||
private getAllAccounts(): string[] {
|
||||
const files = fs.readdirSync(this.dataDir)
|
||||
const emailSet = new Set<string>()
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.json')) continue
|
||||
const parts = file.split('_')
|
||||
if (parts.length >= 2) {
|
||||
const email = parts[0]
|
||||
if (email) emailSet.add(email)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(emailSet)
|
||||
}
|
||||
}
|
||||
// Placeholder kept for backward compatibility with older imports.
|
||||
// New code should implement its own reporting or use webhooks.
|
||||
export {}
|
||||
|
||||
@@ -197,35 +197,32 @@ export class ConfigValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Check schedule
|
||||
if (config.schedule?.enabled) {
|
||||
if (!config.schedule.timeZone) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
field: 'schedule.timeZone',
|
||||
message: 'No timeZone specified, defaulting to UTC',
|
||||
suggestion: 'Set your local timezone (e.g., America/New_York)'
|
||||
})
|
||||
}
|
||||
const legacySchedule = (config as unknown as { schedule?: unknown }).schedule
|
||||
if (legacySchedule !== undefined) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
field: 'schedule',
|
||||
message: 'Legacy schedule block detected.',
|
||||
suggestion: 'Remove schedule.* entries and configure OS-level scheduling (docs/schedule.md).'
|
||||
})
|
||||
}
|
||||
|
||||
const useAmPm = config.schedule.useAmPm
|
||||
const time12 = (config.schedule as unknown as Record<string, unknown>)['time12']
|
||||
const time24 = (config.schedule as unknown as Record<string, unknown>)['time24']
|
||||
if (config.legacy?.diagnosticsConfigured) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
field: 'diagnostics',
|
||||
message: 'Unrecognized diagnostics.* block in config.jsonc.',
|
||||
suggestion: 'Delete the diagnostics section; logs and webhooks now cover troubleshooting.'
|
||||
})
|
||||
}
|
||||
|
||||
if (useAmPm === true && (!time12 || (typeof time12 === 'string' && time12.trim() === ''))) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
field: 'schedule.time12',
|
||||
message: 'useAmPm is true but time12 is empty'
|
||||
})
|
||||
}
|
||||
if (useAmPm === false && (!time24 || (typeof time24 === 'string' && time24.trim() === ''))) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
field: 'schedule.time24',
|
||||
message: 'useAmPm is false but time24 is empty'
|
||||
})
|
||||
}
|
||||
if (config.legacy?.analyticsConfigured) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
field: 'analytics',
|
||||
message: 'Unrecognized analytics.* block in config.jsonc.',
|
||||
suggestion: 'Delete the analytics section because those values are ignored.'
|
||||
})
|
||||
}
|
||||
|
||||
// Check workers
|
||||
@@ -248,27 +245,6 @@ export class ConfigValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagnostics
|
||||
if (config.diagnostics?.enabled) {
|
||||
const maxPerRun = config.diagnostics.maxPerRun || 2
|
||||
if (maxPerRun > 20) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
field: 'diagnostics.maxPerRun',
|
||||
message: 'Very high maxPerRun may fill disk quickly'
|
||||
})
|
||||
}
|
||||
|
||||
const retention = config.diagnostics.retentionDays || 7
|
||||
if (retention > 90) {
|
||||
issues.push({
|
||||
severity: 'info',
|
||||
field: 'diagnostics.retentionDays',
|
||||
message: 'Long retention period - monitor disk usage'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const valid = !issues.some(i => i.severity === 'error')
|
||||
return { valid, issues }
|
||||
}
|
||||
|
||||
@@ -1,74 +1,3 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import type { Page } from 'rebrowser-playwright'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
export type DiagnosticsScope = 'default' | 'security'
|
||||
|
||||
export interface DiagnosticsOptions {
|
||||
scope?: DiagnosticsScope
|
||||
skipSlot?: boolean
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export async function captureDiagnostics(bot: MicrosoftRewardsBot, page: Page, rawLabel: string, options?: DiagnosticsOptions): Promise<void> {
|
||||
try {
|
||||
const scope: DiagnosticsScope = options?.scope ?? 'default'
|
||||
const cfg = bot.config?.diagnostics ?? {}
|
||||
const forceCapture = options?.force === true || scope === 'security'
|
||||
if (!forceCapture && cfg.enabled === false) return
|
||||
|
||||
if (scope === 'default') {
|
||||
const maxPerRun = typeof cfg.maxPerRun === 'number' ? cfg.maxPerRun : 8
|
||||
if (!options?.skipSlot && !bot.tryReserveDiagSlot(maxPerRun)) return
|
||||
}
|
||||
|
||||
const saveScreenshot = scope === 'security' ? true : cfg.saveScreenshot !== false
|
||||
const saveHtml = scope === 'security' ? true : cfg.saveHtml !== false
|
||||
if (!saveScreenshot && !saveHtml) return
|
||||
|
||||
const safeLabel = rawLabel.replace(/[^a-z0-9-_]/gi, '_').slice(0, 64) || 'capture'
|
||||
const now = new Date()
|
||||
const timestamp = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
|
||||
let dir: string
|
||||
if (scope === 'security') {
|
||||
const base = path.join(process.cwd(), 'diagnostics', 'security-incidents')
|
||||
fs.mkdirSync(base, { recursive: true })
|
||||
const sub = `${now.toISOString().replace(/[:.]/g, '-')}-${safeLabel}`
|
||||
dir = path.join(base, sub)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
} else {
|
||||
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
dir = path.join(process.cwd(), 'reports', day)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
if (saveScreenshot) {
|
||||
const shotName = scope === 'security' ? 'page.png' : `${timestamp}_${safeLabel}.png`
|
||||
const shotPath = path.join(dir, shotName)
|
||||
await page.screenshot({ path: shotPath }).catch(() => {})
|
||||
if (scope === 'security') {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved security screenshot to ${shotPath}`)
|
||||
} else {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved diagnostics screenshot to ${shotPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (saveHtml) {
|
||||
const htmlName = scope === 'security' ? 'page.html' : `${timestamp}_${safeLabel}.html`
|
||||
const htmlPath = path.join(dir, htmlName)
|
||||
try {
|
||||
const html = await page.content()
|
||||
await fs.promises.writeFile(htmlPath, html, 'utf-8')
|
||||
if (scope === 'security') {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved security HTML to ${htmlPath}`)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
bot.log(bot.isMobile, 'DIAG', `Failed to capture diagnostics: ${error instanceof Error ? error.message : error}`, 'warn')
|
||||
}
|
||||
}
|
||||
// Placeholder kept for backward compatibility with older imports.
|
||||
// New code should handle troubleshooting through logging and webhooks instead.
|
||||
export {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from 'path'
|
||||
|
||||
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config, ConfigSaveFingerprint } from '../interface/Config'
|
||||
import { Config, ConfigLegacyFlags, ConfigSaveFingerprint } from '../interface/Config'
|
||||
|
||||
let configCache: Config
|
||||
let configSourcePath = ''
|
||||
@@ -168,15 +168,6 @@ function normalizeConfig(raw: unknown): Config {
|
||||
riskThreshold: typeof riskRaw.riskThreshold === 'number' ? riskRaw.riskThreshold : undefined
|
||||
} : undefined
|
||||
|
||||
const analyticsRaw = (n.analytics ?? {}) as Record<string, unknown>
|
||||
const hasAnalyticsCfg = Object.keys(analyticsRaw).length > 0
|
||||
const analytics = hasAnalyticsCfg ? {
|
||||
enabled: analyticsRaw.enabled === true,
|
||||
retentionDays: typeof analyticsRaw.retentionDays === 'number' ? analyticsRaw.retentionDays : undefined,
|
||||
exportMarkdown: analyticsRaw.exportMarkdown === true,
|
||||
webhookSummary: analyticsRaw.webhookSummary === true
|
||||
} : undefined
|
||||
|
||||
const queryDiversityRaw = (n.queryDiversity ?? {}) as Record<string, unknown>
|
||||
const hasQueryCfg = Object.keys(queryDiversityRaw).length > 0
|
||||
const queryDiversity = hasQueryCfg ? {
|
||||
@@ -197,6 +188,15 @@ function normalizeConfig(raw: unknown): Config {
|
||||
skipCompletedAccounts: jobStateRaw.skipCompletedAccounts !== false
|
||||
}
|
||||
|
||||
const legacy: ConfigLegacyFlags = {}
|
||||
if (typeof n.diagnostics !== 'undefined') {
|
||||
legacy.diagnosticsConfigured = true
|
||||
}
|
||||
if (typeof n.analytics !== 'undefined') {
|
||||
legacy.analyticsConfigured = true
|
||||
}
|
||||
const hasLegacyFlags = legacy.diagnosticsConfigured === true || legacy.analyticsConfigured === true
|
||||
|
||||
const cfg: Config = {
|
||||
baseURL: n.baseURL ?? 'https://rewards.bing.com',
|
||||
sessionPath: n.sessionPath ?? 'sessions',
|
||||
@@ -219,17 +219,15 @@ function normalizeConfig(raw: unknown): Config {
|
||||
webhook,
|
||||
conclusionWebhook,
|
||||
ntfy,
|
||||
diagnostics: n.diagnostics,
|
||||
update: n.update,
|
||||
schedule: n.schedule,
|
||||
passesPerRun: passesPerRun,
|
||||
vacation: n.vacation,
|
||||
buyMode: { enabled: buyModeEnabled, maxMinutes: buyModeMax },
|
||||
crashRecovery: n.crashRecovery || {},
|
||||
riskManagement,
|
||||
analytics,
|
||||
dryRun,
|
||||
queryDiversity
|
||||
queryDiversity,
|
||||
legacy: hasLegacyFlags ? legacy : undefined
|
||||
}
|
||||
|
||||
return cfg
|
||||
|
||||
@@ -32,7 +32,6 @@ export class StartupValidator {
|
||||
this.validateEnvironment()
|
||||
this.validateFileSystem(config)
|
||||
this.validateBrowserSettings(config)
|
||||
this.validateScheduleSettings(config)
|
||||
this.validateNetworkSettings(config)
|
||||
this.validateWorkerSettings(config)
|
||||
this.validateSearchSettings(config)
|
||||
@@ -173,6 +172,16 @@ export class StartupValidator {
|
||||
}
|
||||
|
||||
private validateConfig(config: Config): void {
|
||||
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
|
||||
if (maybeSchedule !== undefined) {
|
||||
this.addWarning(
|
||||
'config',
|
||||
'Legacy schedule settings detected in config.jsonc.',
|
||||
'Remove schedule.* entries and use your operating system scheduler.',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
// Headless mode in Docker
|
||||
if (process.env.FORCE_HEADLESS === '1' && config.headless === false) {
|
||||
this.addWarning(
|
||||
@@ -330,20 +339,13 @@ export class StartupValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagnostics directory if enabled
|
||||
if (config.diagnostics?.enabled === true) {
|
||||
const diagPath = path.join(process.cwd(), 'diagnostics')
|
||||
if (!fs.existsSync(diagPath)) {
|
||||
try {
|
||||
fs.mkdirSync(diagPath, { recursive: true })
|
||||
} catch (error) {
|
||||
this.addWarning(
|
||||
'filesystem',
|
||||
'Cannot create diagnostics directory',
|
||||
'Screenshots and HTML snapshots will not be saved'
|
||||
)
|
||||
}
|
||||
}
|
||||
if (config.legacy?.diagnosticsConfigured || config.legacy?.analyticsConfigured) {
|
||||
this.addWarning(
|
||||
'filesystem',
|
||||
'Unrecognized diagnostics/analytics block detected in config.jsonc',
|
||||
'Remove those sections to keep the file aligned with the current schema.',
|
||||
'docs/diagnostics.md'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,60 +370,6 @@ export class StartupValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private validateScheduleSettings(config: Config): void {
|
||||
if (config.schedule?.enabled === true) {
|
||||
// Time format validation
|
||||
const schedRec = config.schedule as Record<string, unknown>
|
||||
const useAmPm = schedRec.useAmPm
|
||||
const time12 = typeof schedRec.time12 === 'string' ? schedRec.time12 : ''
|
||||
const time24 = typeof schedRec.time24 === 'string' ? schedRec.time24 : ''
|
||||
|
||||
if (useAmPm === true && (!time12 || time12.trim() === '')) {
|
||||
this.addError(
|
||||
'schedule',
|
||||
'Schedule enabled with useAmPm=true but time12 is missing',
|
||||
'Add time12 field (e.g., "9:00 AM") or set useAmPm=false',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
if (useAmPm === false && (!time24 || time24.trim() === '')) {
|
||||
this.addError(
|
||||
'schedule',
|
||||
'Schedule enabled with useAmPm=false but time24 is missing',
|
||||
'Add time24 field (e.g., "09:00") or set useAmPm=true',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
// Timezone validation
|
||||
const tz = config.schedule.timeZone || 'UTC'
|
||||
try {
|
||||
Intl.DateTimeFormat(undefined, { timeZone: tz })
|
||||
} catch {
|
||||
this.addError(
|
||||
'schedule',
|
||||
`Invalid timezone: ${tz}`,
|
||||
'Use a valid IANA timezone (e.g., "America/New_York", "Europe/Paris")',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
// Vacation mode check
|
||||
if (config.vacation?.enabled === true) {
|
||||
if (config.vacation.minDays && config.vacation.maxDays) {
|
||||
if (config.vacation.minDays > config.vacation.maxDays) {
|
||||
this.addError(
|
||||
'schedule',
|
||||
`Vacation minDays (${config.vacation.minDays}) > maxDays (${config.vacation.maxDays})`,
|
||||
'Set minDays <= maxDays (e.g., minDays: 2, maxDays: 4)'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateNetworkSettings(config: Config): void {
|
||||
// Webhook validation
|
||||
if (config.webhook?.enabled === true) {
|
||||
@@ -651,8 +599,6 @@ export class StartupValidator {
|
||||
)
|
||||
}
|
||||
|
||||
// Removed diagnostics warning - reports/ folder with masked emails is safe for debugging
|
||||
|
||||
// Proxy exposure check
|
||||
if (config.proxy?.proxyGoogleTrends === false && config.proxy?.proxyBingTerms === false) {
|
||||
this.addWarning(
|
||||
|
||||
Reference in New Issue
Block a user