Steps 1-11, still need to do 12

This commit is contained in:
2025-08-07 15:29:23 -05:00
parent b66a516d20
commit 4019e921d3
15 changed files with 3731 additions and 55 deletions

View File

@@ -23,18 +23,32 @@ class ProgressService {
* @param {Object} config - Configuration object with operation details
* @param {string} config.targetTag - The tag being targeted
* @param {number} config.priceAdjustmentPercentage - The percentage adjustment
* @param {Object} schedulingContext - Optional scheduling context
* @returns {Promise<void>}
*/
async logOperationStart(config) {
async logOperationStart(config, schedulingContext = null) {
const timestamp = this.formatTimestamp();
const content = `
## Price Update Operation - ${timestamp}
const operationTitle =
schedulingContext && schedulingContext.isScheduled
? "Scheduled Price Update Operation"
: "Price Update Operation";
let content = `
## ${operationTitle} - ${timestamp}
**Configuration:**
- Target Tag: ${config.targetTag}
- Price Adjustment: ${config.priceAdjustmentPercentage}%
- Started: ${timestamp}
`;
if (schedulingContext && schedulingContext.isScheduled) {
content += `- Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}
- Original Schedule Input: ${schedulingContext.originalInput}
`;
}
content += `
**Progress:**
`;
@@ -45,18 +59,32 @@ class ProgressService {
* Logs the start of a price rollback operation (Requirements 7.1, 8.3)
* @param {Object} config - Configuration object with operation details
* @param {string} config.targetTag - The tag being targeted
* @param {Object} schedulingContext - Optional scheduling context
* @returns {Promise<void>}
*/
async logRollbackStart(config) {
async logRollbackStart(config, schedulingContext = null) {
const timestamp = this.formatTimestamp();
const content = `
## Price Rollback Operation - ${timestamp}
const operationTitle =
schedulingContext && schedulingContext.isScheduled
? "Scheduled Price Rollback Operation"
: "Price Rollback Operation";
let content = `
## ${operationTitle} - ${timestamp}
**Configuration:**
- Target Tag: ${config.targetTag}
- Operation Mode: rollback
- Started: ${timestamp}
`;
if (schedulingContext && schedulingContext.isScheduled) {
content += `- Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}
- Original Schedule Input: ${schedulingContext.originalInput}
`;
}
content += `
**Progress:**
`;
@@ -117,14 +145,20 @@ class ProgressService {
* @param {string} entry.productTitle - Product title
* @param {string} entry.variantId - Variant ID (optional)
* @param {string} entry.errorMessage - Error message
* @param {Object} schedulingContext - Optional scheduling context
* @returns {Promise<void>}
*/
async logError(entry) {
async logError(entry, schedulingContext = null) {
const timestamp = this.formatTimestamp();
const variantInfo = entry.variantId ? ` - Variant: ${entry.variantId}` : "";
const schedulingInfo =
schedulingContext && schedulingContext.isScheduled
? ` - Scheduled Operation: ${schedulingContext.scheduledTime.toLocaleString()}`
: "";
const content = `- ❌ **${entry.productTitle}** (${entry.productId})${variantInfo}
- Error: ${entry.errorMessage}
- Failed: ${timestamp}
- Failed: ${timestamp}${schedulingInfo}
`;
await this.appendToProgressFile(content);
@@ -257,16 +291,145 @@ ${content}`;
}
/**
* Logs detailed error analysis and patterns
* @param {Array} errors - Array of error objects from operation results
* Logs scheduling confirmation to progress file (Requirements 2.1, 2.3)
* @param {Object} schedulingInfo - Scheduling information
* @returns {Promise<void>}
*/
async logErrorAnalysis(errors) {
async logSchedulingConfirmation(schedulingInfo) {
const { scheduledTime, originalInput, operationType, config } =
schedulingInfo;
const timestamp = this.formatTimestamp();
const content = `
## Scheduled Operation Confirmation - ${timestamp}
**Scheduling Details:**
- Operation Type: ${operationType}
- Scheduled Time: ${scheduledTime.toLocaleString()}
- Original Input: ${originalInput}
- Confirmation Time: ${timestamp}
**Operation Configuration:**
- Target Tag: ${config.targetTag}
${
operationType === "update"
? `- Price Adjustment: ${config.priceAdjustmentPercentage}%`
: ""
}
- Shop Domain: ${config.shopDomain}
**Status:** Waiting for scheduled execution time
`;
await this.appendToProgressFile(content);
}
/**
* Logs scheduled execution start to progress file (Requirements 2.3, 5.4)
* @param {Object} executionInfo - Execution information
* @returns {Promise<void>}
*/
async logScheduledExecutionStart(executionInfo) {
const { scheduledTime, actualTime, operationType } = executionInfo;
const timestamp = this.formatTimestamp();
const delay = actualTime.getTime() - scheduledTime.getTime();
const delayText =
Math.abs(delay) < 1000
? "on time"
: delay > 0
? `${Math.round(delay / 1000)}s late`
: `${Math.round(Math.abs(delay) / 1000)}s early`;
const content = `
**Scheduled Execution Started - ${timestamp}**
- Operation Type: ${operationType}
- Scheduled Time: ${scheduledTime.toLocaleString()}
- Actual Start Time: ${actualTime.toLocaleString()}
- Timing: ${delayText}
`;
await this.appendToProgressFile(content);
}
/**
* Logs scheduled operation cancellation to progress file (Requirements 3.1, 3.2)
* @param {Object} cancellationInfo - Cancellation information
* @returns {Promise<void>}
*/
async logScheduledOperationCancellation(cancellationInfo) {
const { scheduledTime, cancelledTime, operationType, reason } =
cancellationInfo;
const timestamp = this.formatTimestamp();
const timeRemaining = scheduledTime.getTime() - cancelledTime.getTime();
const remainingText = this.formatTimeRemaining(timeRemaining);
const content = `
## Scheduled Operation Cancelled - ${timestamp}
**Cancellation Details:**
- Operation Type: ${operationType}
- Scheduled Time: ${scheduledTime.toLocaleString()}
- Cancelled Time: ${cancelledTime.toLocaleString()}
- Time Remaining: ${remainingText}
- Reason: ${reason}
**Status:** Operation cancelled before execution
---
`;
await this.appendToProgressFile(content);
}
/**
* Format time remaining into human-readable string
* @param {number} milliseconds - Time remaining in milliseconds
* @returns {string} Formatted time string (e.g., "2h 30m 15s")
*/
formatTimeRemaining(milliseconds) {
if (milliseconds <= 0) {
return "0s";
}
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
const remainingMinutes = minutes % 60;
const remainingSeconds = seconds % 60;
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (remainingHours > 0) parts.push(`${remainingHours}h`);
if (remainingMinutes > 0) parts.push(`${remainingMinutes}m`);
if (remainingSeconds > 0 || parts.length === 0)
parts.push(`${remainingSeconds}s`);
return parts.join(" ");
}
/**
* Logs detailed error analysis and patterns
* @param {Array} errors - Array of error objects from operation results
* @param {Object} schedulingContext - Optional scheduling context
* @returns {Promise<void>}
*/
async logErrorAnalysis(errors, schedulingContext = null) {
if (!errors || errors.length === 0) {
return;
}
const timestamp = this.formatTimestamp();
const analysisTitle =
schedulingContext && schedulingContext.isScheduled
? "Scheduled Operation Error Analysis"
: "Error Analysis";
// Categorize errors by type
const errorCategories = {};
@@ -310,8 +473,18 @@ ${content}`;
});
let content = `
**Error Analysis - ${timestamp}**
**${analysisTitle} - ${timestamp}**
`;
if (schedulingContext && schedulingContext.isScheduled) {
content += `
**Scheduling Context:**
- Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}
- Original Schedule Input: ${schedulingContext.originalInput}
`;
}
content += `
**Error Summary by Category:**
`;

640
src/services/schedule.js Normal file
View File

@@ -0,0 +1,640 @@
/**
* ScheduleService - Handles scheduling functionality for delayed execution
* Supports datetime parsing, validation, delay calculation, and countdown display
*/
class ScheduleService {
constructor(logger) {
this.logger = logger;
this.cancelRequested = false;
this.countdownInterval = null;
this.currentTimeoutId = null;
}
/**
* Parse and validate scheduled time from environment variable
* @param {string} scheduledTimeString - ISO 8601 datetime string
* @returns {Date} Parsed date object
* @throws {Error} If datetime format is invalid or in the past
*/
parseScheduledTime(scheduledTimeString) {
// Enhanced input validation with clear error messages
if (!scheduledTimeString) {
throw new Error(
"❌ Scheduled time is required but not provided.\n" +
"💡 Set the SCHEDULED_EXECUTION_TIME environment variable with a valid datetime.\n" +
"📝 Example: SCHEDULED_EXECUTION_TIME='2024-12-25T10:30:00'"
);
}
if (typeof scheduledTimeString !== "string") {
throw new Error(
"❌ Scheduled time must be provided as a string.\n" +
`📊 Received type: ${typeof scheduledTimeString}\n` +
"💡 Ensure SCHEDULED_EXECUTION_TIME is set as a string value."
);
}
const trimmedInput = scheduledTimeString.trim();
if (trimmedInput === "") {
throw new Error(
"❌ Scheduled time cannot be empty or contain only whitespace.\n" +
"💡 Provide a valid ISO 8601 datetime string.\n" +
"📝 Example: SCHEDULED_EXECUTION_TIME='2024-12-25T10:30:00'"
);
}
// Enhanced datetime format validation with detailed error messages
const iso8601Regex =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)?$/;
if (!iso8601Regex.test(trimmedInput)) {
const commonFormats = [
"YYYY-MM-DDTHH:MM:SS (e.g., '2024-12-25T10:30:00')",
"YYYY-MM-DDTHH:MM:SSZ (e.g., '2024-12-25T10:30:00Z')",
"YYYY-MM-DDTHH:MM:SS+HH:MM (e.g., '2024-12-25T10:30:00-05:00')",
"YYYY-MM-DDTHH:MM:SS.sssZ (e.g., '2024-12-25T10:30:00.000Z')",
];
throw new Error(
`❌ Invalid datetime format: "${trimmedInput}"\n\n` +
"📋 The datetime must be in ISO 8601 format. Accepted formats:\n" +
commonFormats.map((format) => `${format}`).join("\n") +
"\n\n" +
"🔍 Common issues to check:\n" +
" • Use 'T' to separate date and time (not space)\n" +
" • Use 24-hour format (00-23 for hours)\n" +
" • Ensure month and day are two digits (01-12, 01-31)\n" +
" • Include timezone if needed (+HH:MM, -HH:MM, or Z for UTC)\n\n" +
"💡 Tip: Use your local timezone or add 'Z' for UTC"
);
}
// Additional validation for datetime component values before parsing
const dateParts = trimmedInput.match(
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/
);
if (dateParts) {
const [, year, month, day, hour, minute, second] = dateParts;
const yearNum = parseInt(year);
const monthNum = parseInt(month);
const dayNum = parseInt(day);
const hourNum = parseInt(hour);
const minuteNum = parseInt(minute);
const secondNum = parseInt(second);
const valueIssues = [];
if (yearNum < 1970 || yearNum > 3000)
valueIssues.push(`Year ${year} seems unusual (expected 1970-3000)`);
if (monthNum < 1 || monthNum > 12)
valueIssues.push(`Month ${month} must be 01-12`);
if (dayNum < 1 || dayNum > 31)
valueIssues.push(`Day ${day} must be 01-31`);
if (hourNum > 23)
valueIssues.push(`Hour ${hour} must be 00-23 (24-hour format)`);
if (minuteNum > 59) valueIssues.push(`Minute ${minute} must be 00-59`);
if (secondNum > 59) valueIssues.push(`Second ${second} must be 00-59`);
if (valueIssues.length > 0) {
throw new Error(
`❌ Invalid datetime values: "${trimmedInput}"\n\n` +
"🔍 Detected issues:\n" +
valueIssues.map((issue) => `${issue}`).join("\n") +
"\n\n" +
"💡 Common fixes:\n" +
" • Check if the date exists (e.g., February 30th doesn't exist)\n" +
" • Verify month is 01-12, not 0-11\n" +
" • Ensure day is valid for the given month and year\n" +
" • Use 24-hour format for time (00-23 for hours)"
);
}
}
// Attempt to parse the datetime with enhanced error handling
let scheduledTime;
try {
scheduledTime = new Date(trimmedInput);
} catch (parseError) {
throw new Error(
`❌ Failed to parse datetime: "${trimmedInput}"\n` +
`🔧 Parse error: ${parseError.message}\n` +
"💡 Please verify the datetime values are valid (e.g., month 1-12, day 1-31)"
);
}
// Enhanced validation for parsed date
if (isNaN(scheduledTime.getTime())) {
// Provide specific guidance based on common datetime issues
const dateParts = trimmedInput.match(
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/
);
let specificGuidance = "";
if (dateParts) {
const [, year, month, day, hour, minute, second] = dateParts;
const yearNum = parseInt(year);
const monthNum = parseInt(month);
const dayNum = parseInt(day);
const hourNum = parseInt(hour);
const minuteNum = parseInt(minute);
const secondNum = parseInt(second);
const issues = [];
if (yearNum < 1970 || yearNum > 3000)
issues.push(`Year ${year} seems unusual`);
if (monthNum < 1 || monthNum > 12)
issues.push(`Month ${month} must be 01-12`);
if (dayNum < 1 || dayNum > 31) issues.push(`Day ${day} must be 01-31`);
if (hourNum > 23) issues.push(`Hour ${hour} must be 00-23`);
if (minuteNum > 59) issues.push(`Minute ${minute} must be 00-59`);
if (secondNum > 59) issues.push(`Second ${second} must be 00-59`);
if (issues.length > 0) {
specificGuidance =
"\n🔍 Detected issues:\n" +
issues.map((issue) => `${issue}`).join("\n");
}
}
throw new Error(
`❌ Invalid datetime values: "${trimmedInput}"\n` +
"📊 The datetime format is correct, but the values are invalid.\n" +
specificGuidance +
"\n\n" +
"💡 Common fixes:\n" +
" • Check if the date exists (e.g., February 30th doesn't exist)\n" +
" • Verify month is 01-12, not 0-11\n" +
" • Ensure day is valid for the given month and year\n" +
" • Use 24-hour format for time (00-23 for hours)"
);
}
const currentTime = new Date();
// Enhanced past datetime validation with helpful context
if (scheduledTime <= currentTime) {
const timeDifference = currentTime.getTime() - scheduledTime.getTime();
const minutesDiff = Math.floor(timeDifference / (1000 * 60));
const hoursDiff = Math.floor(timeDifference / (1000 * 60 * 60));
const daysDiff = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
let timeAgoText = "";
if (daysDiff > 0) {
timeAgoText = `${daysDiff} day${daysDiff > 1 ? "s" : ""} ago`;
} else if (hoursDiff > 0) {
timeAgoText = `${hoursDiff} hour${hoursDiff > 1 ? "s" : ""} ago`;
} else if (minutesDiff > 0) {
timeAgoText = `${minutesDiff} minute${minutesDiff > 1 ? "s" : ""} ago`;
} else {
timeAgoText = "just now";
}
throw new Error(
`❌ Scheduled time is in the past: "${trimmedInput}"\n\n` +
`📅 Scheduled time: ${scheduledTime.toLocaleString()} (${timeAgoText})\n` +
`🕐 Current time: ${currentTime.toLocaleString()}\n\n` +
"💡 Solutions:\n" +
" • Set a future datetime for the scheduled operation\n" +
" • Check your system clock if the time seems incorrect\n" +
" • Consider timezone differences if using a specific timezone\n\n" +
"📝 Example for 1 hour from now:\n" +
` SCHEDULED_EXECUTION_TIME='${new Date(
currentTime.getTime() + 60 * 60 * 1000
)
.toISOString()
.slice(0, 19)}'`
);
}
// Enhanced distant future validation with detailed warning
const sevenDaysFromNow = new Date(
currentTime.getTime() + 7 * 24 * 60 * 60 * 1000
);
if (scheduledTime > sevenDaysFromNow) {
const daysDiff = Math.ceil(
(scheduledTime.getTime() - currentTime.getTime()) /
(1000 * 60 * 60 * 24)
);
// Display comprehensive warning with context
console.warn(
`\n⚠️ WARNING: Distant Future Scheduling Detected\n` +
`📅 Scheduled time: ${scheduledTime.toLocaleString()}\n` +
`📊 Days from now: ${daysDiff} days\n` +
`🕐 Current time: ${currentTime.toLocaleString()}\n\n` +
`🤔 This operation is scheduled more than 7 days in the future.\n` +
`💭 Please verify this is intentional, as:\n` +
` • Long-running processes may be interrupted by system restarts\n` +
` • Product data or pricing strategies might change\n` +
` • API tokens or store configuration could be updated\n\n` +
`✅ If this is correct, the operation will proceed as scheduled.\n` +
`❌ If this is a mistake, press Ctrl+C to cancel and update the datetime.\n`
);
// Log the warning for audit purposes
if (this.logger) {
this.logger
.warning(
`Scheduled operation set for distant future: ${daysDiff} days from now (${scheduledTime.toISOString()})`
)
.catch((err) => {
console.error("Failed to log distant future warning:", err.message);
});
}
}
return scheduledTime;
}
/**
* Calculate milliseconds until scheduled execution
* @param {Date} scheduledTime - Target execution time
* @returns {number} Milliseconds until execution
*/
calculateDelay(scheduledTime) {
const currentTime = new Date();
const delay = scheduledTime.getTime() - currentTime.getTime();
// Ensure delay is not negative (shouldn't happen after validation, but safety check)
return Math.max(0, delay);
}
/**
* Format time remaining into human-readable string
* @param {number} milliseconds - Time remaining in milliseconds
* @returns {string} Formatted time string (e.g., "2h 30m 15s")
*/
formatTimeRemaining(milliseconds) {
if (milliseconds <= 0) {
return "0s";
}
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
const remainingMinutes = minutes % 60;
const remainingSeconds = seconds % 60;
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (remainingHours > 0) parts.push(`${remainingHours}h`);
if (remainingMinutes > 0) parts.push(`${remainingMinutes}m`);
if (remainingSeconds > 0 || parts.length === 0)
parts.push(`${remainingSeconds}s`);
return parts.join(" ");
}
/**
* Display scheduling confirmation and countdown
* @param {Date} scheduledTime - Target execution time
* @returns {Promise<void>}
*/
async displayScheduleInfo(scheduledTime) {
const delay = this.calculateDelay(scheduledTime);
const timeRemaining = this.formatTimeRemaining(delay);
// Display initial scheduling confirmation
await this.logger.info(
`Operation scheduled for: ${scheduledTime.toLocaleString()}`
);
await this.logger.info(`Time remaining: ${timeRemaining}`);
await this.logger.info("Press Ctrl+C to cancel the scheduled operation");
// Start countdown display (update every 30 seconds for efficiency)
this.startCountdownDisplay(scheduledTime);
}
/**
* Start countdown display with periodic updates
* @param {Date} scheduledTime - Target execution time
*/
startCountdownDisplay(scheduledTime) {
// Clear any existing countdown
this.stopCountdownDisplay();
// Update countdown every 30 seconds
this.countdownInterval = setInterval(() => {
if (this.cancelRequested) {
this.stopCountdownDisplay();
return;
}
const delay = this.calculateDelay(scheduledTime);
if (delay <= 0) {
this.stopCountdownDisplay();
return;
}
const timeRemaining = this.formatTimeRemaining(delay);
// Use console.log for countdown updates to avoid async issues in interval
console.log(
`[${new Date()
.toISOString()
.replace("T", " ")
.replace(/\.\d{3}Z$/, "")}] INFO: Time remaining: ${timeRemaining}`
);
}, 30000); // Update every 30 seconds
}
/**
* Stop countdown display
*/
stopCountdownDisplay() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
}
/**
* Wait until scheduled time with cancellation support
* @param {Date} scheduledTime - Target execution time
* @param {Function} onCancel - Callback function to execute on cancellation
* @returns {Promise<boolean>} True if execution should proceed, false if cancelled
*/
async waitUntilScheduledTime(scheduledTime, onCancel) {
const delay = this.calculateDelay(scheduledTime);
if (delay <= 0) {
return true; // Execute immediately
}
return new Promise((resolve) => {
let resolved = false;
// Set timeout for scheduled execution
const timeoutId = setTimeout(() => {
if (resolved) return;
resolved = true;
this.stopCountdownDisplay();
this.currentTimeoutId = null;
if (!this.cancelRequested) {
// Use console.log for immediate execution message
console.log(
`[${new Date()
.toISOString()
.replace("T", " ")
.replace(
/\.\d{3}Z$/,
""
)}] INFO: Scheduled time reached. Starting operation...`
);
resolve(true);
} else {
resolve(false);
}
}, delay);
// Store timeout ID for cleanup
this.currentTimeoutId = timeoutId;
// Set up cancellation check mechanism
// The main signal handlers will call cleanup() which sets cancelRequested
const checkCancellation = () => {
if (resolved) return;
if (this.cancelRequested) {
resolved = true;
clearTimeout(timeoutId);
this.stopCountdownDisplay();
this.currentTimeoutId = null;
if (onCancel && typeof onCancel === "function") {
onCancel();
}
resolve(false);
} else if (!resolved) {
// Check again in 100ms
setTimeout(checkCancellation, 100);
}
};
// Start cancellation checking
setTimeout(checkCancellation, 100);
});
}
/**
* Execute the scheduled operation
* @param {Function} operationCallback - The operation to execute
* @returns {Promise<number>} Exit code from the operation
*/
async executeScheduledOperation(operationCallback) {
try {
await this.logger.info("Executing scheduled operation...");
const result = await operationCallback();
await this.logger.info("Scheduled operation completed successfully");
return result || 0;
} catch (error) {
await this.logger.error(`Scheduled operation failed: ${error.message}`);
throw error;
}
}
/**
* Clean up resources and request cancellation
*/
cleanup() {
this.stopCountdownDisplay();
this.cancelRequested = true;
// Clear any active timeout
if (this.currentTimeoutId) {
clearTimeout(this.currentTimeoutId);
this.currentTimeoutId = null;
}
}
/**
* Reset the service state (for testing or reuse)
*/
reset() {
this.stopCountdownDisplay();
this.cancelRequested = false;
if (this.currentTimeoutId) {
clearTimeout(this.currentTimeoutId);
this.currentTimeoutId = null;
}
}
/**
* Validate scheduling configuration and provide comprehensive error handling
* @param {string} scheduledTimeString - Raw scheduled time string from environment
* @returns {Object} Validation result with parsed time or error details
*/
validateSchedulingConfiguration(scheduledTimeString) {
try {
const scheduledTime = this.parseScheduledTime(scheduledTimeString);
return {
isValid: true,
scheduledTime: scheduledTime,
originalInput: scheduledTimeString,
validationError: null,
warningMessage: null,
};
} catch (error) {
// Categorize the error for better handling with more specific detection
let errorCategory = "unknown";
let helpfulSuggestions = [];
// Check for missing input first (highest priority)
if (
error.message.includes("required") ||
error.message.includes("empty") ||
error.message.includes("provided")
) {
errorCategory = "missing_input";
helpfulSuggestions = [
"Set the SCHEDULED_EXECUTION_TIME environment variable",
"Ensure the value is not empty or whitespace only",
"Use a valid ISO 8601 datetime string",
];
}
// Check for past time (high priority)
else if (error.message.includes("past")) {
errorCategory = "past_time";
helpfulSuggestions = [
"Set a future datetime for the scheduled operation",
"Check your system clock if the time seems incorrect",
"Consider timezone differences when scheduling",
];
}
// Check for format issues first (more specific patterns)
else if (
error.message.includes("❌ Invalid datetime format") ||
error.message.includes("Invalid datetime format") ||
error.message.includes("ISO 8601")
) {
errorCategory = "format";
helpfulSuggestions = [
"Use ISO 8601 format: YYYY-MM-DDTHH:MM:SS",
"Add timezone if needed: +HH:MM, -HH:MM, or Z for UTC",
"Separate date and time with 'T', not a space",
];
}
// Check for invalid values (month, day, hour issues) - specific patterns
else if (
error.message.includes("❌ Invalid datetime values") ||
error.message.includes("Invalid datetime values") ||
error.message.includes("Month") ||
error.message.includes("Day") ||
error.message.includes("Hour") ||
error.message.includes("must be")
) {
errorCategory = "invalid_values";
helpfulSuggestions = [
"Check if the date exists (e.g., February 30th doesn't exist)",
"Verify month is 01-12, day is valid for the month",
"Use 24-hour format for time (00-23 for hours)",
];
}
// Check for parse errors (catch remaining format-related errors)
else if (
error.message.includes("parse") ||
error.message.includes("format")
) {
errorCategory = "format";
helpfulSuggestions = [
"Use ISO 8601 format: YYYY-MM-DDTHH:MM:SS",
"Add timezone if needed: +HH:MM, -HH:MM, or Z for UTC",
"Separate date and time with 'T', not a space",
];
}
return {
isValid: false,
scheduledTime: null,
originalInput: scheduledTimeString,
validationError: error.message,
errorCategory: errorCategory,
suggestions: helpfulSuggestions,
timestamp: new Date().toISOString(),
};
}
}
/**
* Display comprehensive error information for scheduling failures
* @param {Object} validationResult - Result from validateSchedulingConfiguration
* @returns {Promise<void>}
*/
async displaySchedulingError(validationResult) {
if (validationResult.isValid) {
return; // No error to display
}
// Display error header
console.error("\n" + "=".repeat(60));
console.error("🚨 SCHEDULING CONFIGURATION ERROR");
console.error("=".repeat(60));
// Display the main error message
console.error("\n" + validationResult.validationError);
// Display additional context if available
if (validationResult.originalInput) {
console.error(`\n📝 Input received: "${validationResult.originalInput}"`);
}
// Display category-specific help
if (
validationResult.suggestions &&
validationResult.suggestions.length > 0
) {
console.error("\n💡 Suggestions to fix this issue:");
validationResult.suggestions.forEach((suggestion) => {
console.error(`${suggestion}`);
});
}
// Display general help information
console.error("\n📚 Additional Resources:");
console.error(" • Check .env.example for configuration examples");
console.error(" • Verify your system timezone settings");
console.error(" • Use online ISO 8601 datetime generators if needed");
console.error("\n" + "=".repeat(60) + "\n");
// Log the error to the progress file if logger is available
if (this.logger) {
try {
await this.logger.error(
`Scheduling configuration error: ${validationResult.errorCategory} - ${validationResult.validationError}`
);
} catch (loggingError) {
console.error("Failed to log scheduling error:", loggingError.message);
}
}
}
/**
* Handle scheduling errors with proper exit codes and user guidance
* @param {string} scheduledTimeString - Raw scheduled time string from environment
* @returns {Promise<number>} Exit code (0 for success, 1 for error)
*/
async handleSchedulingValidation(scheduledTimeString) {
const validationResult =
this.validateSchedulingConfiguration(scheduledTimeString);
if (!validationResult.isValid) {
await this.displaySchedulingError(validationResult);
return 1; // Error exit code
}
// If validation passed, store the parsed time for later use
this.validatedScheduledTime = validationResult.scheduledTime;
return 0; // Success exit code
}
}
module.exports = ScheduleService;