diff --git a/examples/app/complex.json b/examples/app/complex.json deleted file mode 100644 index 6c12758..0000000 --- a/examples/app/complex.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "title": "Company Registration", - "properties": { - "session_id": { - "type": "string", - "title": "Session ID", - "ui": { - "element": "input", - "type": "hidden", - "name": "session_id", - "value": "{{session_id}}" - } - }, - "task_id": { - "type": "string", - "title": "Task ID", - "ui": { - "element": "input", - "type": "hidden", - "name": "task_id", - "value": "{{task_id}}" - } - }, - "company": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Company Name", - "ui": { - "element": "input", - "type": "text", - "name": "name", - "class": "form-group", - "order": 1 - } - }, - "address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street Address", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 2 - } - }, - "city": { - "type": "string", - "title": "City", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 3 - } - }, - "country": { - "type": "string", - "title": "Country", - "enum": [ "US", "CA", "UK", "DE", "FR" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 4, - "options": [ - { "value": "US", "text": "United States" }, - { "value": "CA", "text": "Canada" }, - { "value": "UK", "text": "United Kingdom" }, - { "value": "DE", "text": "Germany" }, - { "value": "FR", "text": "France" } - ] - } - }, - "zipCode": { - "type": "string", - "title": "ZIP/Postal Code", - "pattern": "^[0-9]{5}(-[0-9]{4})?$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 5 - } - } - }, - "required": [ "street", "city", "country" ] - }, - "contact": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "Contact Email", - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "order": 6 - } - }, - "phone": { - "type": "string", - "title": "Phone Number", - "ui": { - "element": "input", - "type": "tel", - "class": "form-group", - "order": 7 - } - } - }, - "required": [ "email" ] - } - }, - "required": [ "name", "address", "contact" ] - } - }, - "required": [ "company" ], - "form": { - "class": "company-registration-form", - "action": "/process", - "method": "POST", - "groups": [ - { - "title": { - "text": "Company Information", - "class": "group-header" - }, - "class": "company-info", - "fields": [ "company.name" ] - }, - { - "title": { - "text": "Address Details", - "class": "group-header" - }, - "class": "flex items-start gap-4 address-info", - "fields": [ "company.address.street", "company.address.city", "company.address.country", "company.address.zipCode" ] - }, - { - "title": { - "text": "Contact Information", - "class": "group-header" - }, - "class": "flex items-start gap-4 contact-info", - "fields": [ "company.contact.email", "company.contact.phone" ] - } - ], - "submit": { - "label": "Register Company", - "class": "btn btn-success btn-lg px-4 py-2" - } - } -} diff --git a/examples/app/comprehensive-validation.json b/examples/app/comprehensive-validation.json deleted file mode 100644 index 0a4b792..0000000 --- a/examples/app/comprehensive-validation.json +++ /dev/null @@ -1,681 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "title": "Comprehensive Validation Demo", - "description": "A complete example showcasing all JSON Schema 2020-12 validation features", - "properties": { - "personalInfo": { - "type": "object", - "title": "Personal Information", - "properties": { - "firstName": { - "type": "string", - "title": "First Name", - "minLength": 2, - "maxLength": 50, - "pattern": "^[A-Za-z\\s]+$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 1, - "placeholder": "Enter your first name" - } - }, - "lastName": { - "type": "string", - "title": "Last Name", - "minLength": 2, - "maxLength": 50, - "pattern": "^[A-Za-z\\s]+$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 2 - } - }, - "age": { - "type": "integer", - "title": "Age", - "minimum": 18, - "maximum": 120, - "ui": { - "element": "input", - "type": "number", - "class": "form-group", - "order": 3 - } - }, - "email": { - "type": "string", - "format": "email", - "title": "Email Address", - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "order": 4 - } - }, - "phone": { - "type": "string", - "title": "Phone Number", - "pattern": "^\\+?[1-9]\\d{1,14}$", - "ui": { - "element": "input", - "type": "tel", - "class": "form-group", - "order": 5 - } - }, - "dateOfBirth": { - "type": "string", - "format": "date", - "title": "Date of Birth", - "ui": { - "element": "input", - "type": "date", - "class": "form-group", - "order": 6 - } - }, - "website": { - "type": "string", - "format": "uri", - "title": "Personal Website", - "ui": { - "element": "input", - "type": "url", - "class": "form-group", - "order": 7 - } - } - }, - "required": [ "firstName", "lastName", "email" ], - "dependentRequired": { - "phone": [ "email" ], - "website": [ "email" ] - } - }, - "address": { - "type": "object", - "title": "Address Information", - "properties": { - "street": { - "type": "string", - "title": "Street Address", - "minLength": 5, - "maxLength": 100, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 1 - } - }, - "city": { - "type": "string", - "title": "City", - "minLength": 2, - "maxLength": 50, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 2 - } - }, - "state": { - "type": "string", - "title": "State/Province", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 3 - } - }, - "country": { - "type": "string", - "title": "Country", - "enum": [ "US", "CA", "UK", "DE", "FR", "AU", "JP", "IN" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 4, - "options": [ - { "value": "US", "text": "United States" }, - { "value": "CA", "text": "Canada" }, - { "value": "UK", "text": "United Kingdom" }, - { "value": "DE", "text": "Germany" }, - { "value": "FR", "text": "France" }, - { "value": "AU", "text": "Australia" }, - { "value": "JP", "text": "Japan" }, - { "value": "IN", "text": "India" } - ] - } - }, - "postalCode": { - "type": "string", - "title": "Postal Code", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 5 - } - } - }, - "required": [ "street", "city", "country" ], - "allOf": [ - { - "if": { - "properties": { - "country": { "const": "US" } - } - }, - "then": { - "properties": { - "postalCode": { - "pattern": "^\\d{5}(-\\d{4})?$" - } - } - } - }, - { - "if": { - "properties": { - "country": { "const": "CA" } - } - }, - "then": { - "properties": { - "postalCode": { - "pattern": "^[A-Z]\\d[A-Z] \\d[A-Z]\\d$" - } - } - } - } - ] - }, - "preferences": { - "type": "object", - "title": "User Preferences", - "properties": { - "newsletter": { - "type": "boolean", - "title": "Subscribe to Newsletter", - "default": false, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group", - "order": 1 - } - }, - "notifications": { - "type": "array", - "title": "Notification Types", - "minItems": 1, - "maxItems": 5, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ "email", "sms", "push", "phone", "mail" ] - }, - "ui": { - "element": "select", - "class": "form-group", - "order": 2, - "multiple": true, - "options": [ - { "value": "email", "text": "Email Notifications" }, - { "value": "sms", "text": "SMS Notifications" }, - { "value": "push", "text": "Push Notifications" }, - { "value": "phone", "text": "Phone Calls" }, - { "value": "mail", "text": "Physical Mail" } - ] - } - }, - "theme": { - "type": "string", - "title": "Preferred Theme", - "enum": [ "light", "dark", "auto" ], - "default": "auto", - "ui": { - "element": "select", - "class": "form-group", - "order": 3, - "options": [ - { "value": "light", "text": "Light Theme" }, - { "value": "dark", "text": "Dark Theme" }, - { "value": "auto", "text": "Auto (System)" } - ] - } - }, - "language": { - "type": "string", - "title": "Preferred Language", - "enum": [ "en", "es", "fr", "de", "zh", "ja" ], - "default": "en", - "ui": { - "element": "select", - "class": "form-group", - "order": 4, - "options": [ - { "value": "en", "text": "English" }, - { "value": "es", "text": "Español" }, - { "value": "fr", "text": "Français" }, - { "value": "de", "text": "Deutsch" }, - { "value": "zh", "text": "中文" }, - { "value": "ja", "text": "日本語" } - ] - } - } - } - }, - "financial": { - "type": "object", - "title": "Financial Information", - "properties": { - "annualIncome": { - "type": "number", - "title": "Annual Income", - "minimum": 0, - "maximum": 10000000, - "multipleOf": 1000, - "ui": { - "element": "input", - "type": "number", - "class": "form-group", - "order": 1, - "step": "1000" - } - }, - "creditScore": { - "type": "integer", - "title": "Credit Score", - "minimum": 300, - "maximum": 850, - "ui": { - "element": "input", - "type": "range", - "class": "form-group", - "order": 2, - "min": "300", - "max": "850" - } - }, - "accountType": { - "type": "string", - "title": "Account Type", - "oneOf": [ - { - "const": "basic", - "title": "Basic Account", - "description": "Free account with basic features" - }, - { - "const": "premium", - "title": "Premium Account", - "description": "Paid account with advanced features" - }, - { - "const": "enterprise", - "title": "Enterprise Account", - "description": "Business account with full features" - } - ], - "ui": { - "element": "select", - "class": "form-group", - "order": 3, - "options": [ - { "value": "basic", "text": "Basic Account" }, - { "value": "premium", "text": "Premium Account" }, - { "value": "enterprise", "text": "Enterprise Account" } - ] - } - } - }, - "if": { - "properties": { - "accountType": { "const": "premium" } - } - }, - "then": { - "properties": { - "paymentMethod": { - "type": "string", - "title": "Payment Method", - "enum": [ "credit_card", "debit_card", "paypal", "bank_transfer" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 4, - "options": [ - { "value": "credit_card", "text": "Credit Card" }, - { "value": "debit_card", "text": "Debit Card" }, - { "value": "paypal", "text": "PayPal" }, - { "value": "bank_transfer", "text": "Bank Transfer" } - ] - } - } - }, - "required": [ "paymentMethod" ] - }, - "else": { - "not": { - "required": [ "paymentMethod" ] - } - } - }, - "skills": { - "type": "array", - "title": "Technical Skills", - "minItems": 1, - "maxItems": 10, - "uniqueItems": true, - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Skill Name", - "minLength": 2, - "maxLength": 50 - }, - "level": { - "type": "string", - "title": "Proficiency Level", - "enum": [ "beginner", "intermediate", "advanced", "expert" ] - }, - "yearsExperience": { - "type": "number", - "title": "Years of Experience", - "minimum": 0, - "maximum": 50, - "multipleOf": 0.5 - } - }, - "required": [ "name", "level" ] - }, - "ui": { - "element": "fieldset", - "class": "skills-section", - "order": 1 - } - }, - "portfolio": { - "type": "object", - "title": "Portfolio", - "properties": { - "projects": { - "type": "array", - "title": "Projects", - "maxItems": 5, - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Project Title", - "minLength": 3, - "maxLength": 100 - }, - "description": { - "type": "string", - "title": "Description", - "maxLength": 500, - "ui": { - "element": "textarea", - "rows": 4, - "class": "form-group" - } - }, - "url": { - "type": "string", - "format": "uri", - "title": "Project URL" - }, - "technologies": { - "type": "array", - "title": "Technologies Used", - "items": { - "type": "string" - }, - "minItems": 1, - "maxItems": 10, - "uniqueItems": true - } - }, - "required": [ "title", "description" ] - } - } - }, - "anyOf": [ - { - "properties": { - "githubUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://github\\.com/", - "title": "GitHub Profile" - } - } - }, - { - "properties": { - "linkedinUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://www\\.linkedin\\.com/", - "title": "LinkedIn Profile" - } - } - }, - { - "properties": { - "personalWebsite": { - "type": "string", - "format": "uri", - "title": "Personal Website" - } - } - } - ] - }, - "comments": { - "type": "string", - "title": "Additional Comments", - "maxLength": 1000, - "ui": { - "element": "textarea", - "class": "form-group", - "rows": 5, - "placeholder": "Any additional information you'd like to share..." - } - }, - "termsAccepted": { - "type": "boolean", - "title": "I accept the terms and conditions", - "const": true, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group required-checkbox" - } - }, - "privacyAccepted": { - "type": "boolean", - "title": "I accept the privacy policy", - "const": true, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group required-checkbox" - } - } - }, - "required": [ "personalInfo", "address", "termsAccepted", "privacyAccepted" ], - "dependentSchemas": { - "financial": { - "properties": { - "personalInfo": { - "properties": { - "age": { - "minimum": 21 - } - } - } - } - } - }, - "form": { - "class": "comprehensive-form", - "action": "/api/submit-comprehensive-form", - "method": "POST", - "enctype": "multipart/form-data", - "groups": [ - { - "title": { - "text": "Personal Information", - "class": "section-header" - }, - "class": "personal-info-section", - "fields": [ - "personalInfo.firstName", - "personalInfo.lastName", - "personalInfo.age", - "personalInfo.email", - "personalInfo.phone", - "personalInfo.dateOfBirth", - "personalInfo.website" - ] - }, - { - "title": { - "text": "Address Information", - "class": "section-header" - }, - "class": "address-section flex flex-wrap gap-4", - "fields": [ - "address.street", - "address.city", - "address.state", - "address.country", - "address.postalCode" - ] - }, - { - "title": { - "text": "Preferences & Settings", - "class": "section-header" - }, - "class": "preferences-section", - "fields": [ - "preferences.newsletter", - "preferences.notifications", - "preferences.theme", - "preferences.language" - ] - }, - { - "title": { - "text": "Financial Information", - "class": "section-header" - }, - "class": "financial-section", - "fields": [ - "financial.annualIncome", - "financial.creditScore", - "financial.accountType" - ] - }, - { - "title": { - "text": "Skills & Portfolio", - "class": "section-header" - }, - "class": "skills-section", - "fields": [ - "skills", - "portfolio" - ] - }, - { - "title": { - "text": "Final Details", - "class": "section-header" - }, - "class": "final-section", - "fields": [ - "comments", - "termsAccepted", - "privacyAccepted" - ] - } - ], - "submit": { - "label": "Submit Application", - "class": "btn btn-primary btn-lg", - "id": "submit-btn" - }, - "reset": { - "label": "Reset Form", - "class": "btn btn-secondary", - "id": "reset-btn" - }, - "buttons": [ - { - "type": "button", - "label": "Save Draft", - "class": "btn btn-outline-secondary", - "id": "save-draft-btn", - "onclick": "saveDraft()" - }, - { - "type": "button", - "label": "Preview", - "class": "btn btn-outline-primary", - "id": "preview-btn", - "onclick": "showPreview()" - } - ] - }, - "examples": [ - { - "personalInfo": { - "firstName": "John", - "lastName": "Doe", - "age": 30, - "email": "john.doe@example.com", - "phone": "+1-555-123-4567", - "dateOfBirth": "1993-05-15", - "website": "https://johndoe.com" - }, - "address": { - "street": "123 Main St", - "city": "New York", - "state": "NY", - "country": "US", - "postalCode": "10001" - }, - "preferences": { - "newsletter": true, - "notifications": [ "email", "push" ], - "theme": "dark", - "language": "en" - }, - "financial": { - "annualIncome": 75000, - "creditScore": 750, - "accountType": "premium", - "paymentMethod": "credit_card" - }, - "termsAccepted": true, - "privacyAccepted": true - } - ] -} diff --git a/examples/app/schema.json b/examples/app/schema.json deleted file mode 100644 index ec92b22..0000000 --- a/examples/app/schema.json +++ /dev/null @@ -1,528 +0,0 @@ -{ - "type": "object", - "properties": { - "page_title": { - "type": "string", - "title": "Main Page Title", - "order": 1, - "ui": { - "element": "h1", - "class": "text-3xl font-bold text-center mb-6", - "name": "page_title", - "content": "Comprehensive HTML Form Demo" - } - }, - "intro_paragraph": { - "type": "string", - "title": "Introduction", - "order": 2, - "ui": { - "element": "p", - "class": "text-gray-600 mb-4", - "name": "intro", - "content": "This form demonstrates all supported HTML DOM elements in the renderer." - } - }, - "divider": { - "type": "string", - "order": 3, - "ui": { - "element": "hr", - "class": "my-6 border-gray-300" - } - }, - "first_name": { - "type": "string", - "title": "First Name", - "placeholder": "Enter your first name", - "order": 10, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "name": "first_name", - "autocomplete": "given-name", - "maxlength": "50" - } - }, - "last_name": { - "type": "string", - "title": "Last Name", - "placeholder": "Enter your last name", - "order": 11, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "name": "last_name", - "autocomplete": "family-name", - "maxlength": "50" - } - }, - "email": { - "type": "email", - "title": "Email Address", - "placeholder": "your.email@example.com", - "order": 12, - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "name": "email", - "autocomplete": "email" - } - }, - "password": { - "type": "password", - "title": "Password", - "placeholder": "Enter a secure password", - "order": 13, - "ui": { - "element": "input", - "type": "password", - "class": "form-group", - "name": "password", - "minlength": "8" - } - }, - "age": { - "type": "number", - "title": "Age", - "order": 14, - "ui": { - "element": "input", - "type": "number", - "class": "form-group", - "name": "age", - "min": "18", - "max": "120" - } - }, - "birth_date": { - "type": "date", - "title": "Birth Date", - "order": 15, - "ui": { - "element": "input", - "type": "date", - "class": "form-group", - "name": "birth_date" - } - }, - "website": { - "type": "url", - "title": "Personal Website", - "placeholder": "https://example.com", - "order": 16, - "ui": { - "element": "input", - "type": "url", - "class": "form-group", - "name": "website" - } - }, - "phone": { - "type": "tel", - "title": "Phone Number", - "placeholder": "+1-555-123-4567", - "order": 17, - "ui": { - "element": "input", - "type": "tel", - "class": "form-group", - "name": "phone" - } - }, - "favorite_color": { - "type": "color", - "title": "Favorite Color", - "order": 18, - "ui": { - "element": "input", - "type": "color", - "class": "form-group", - "name": "favorite_color", - "value": "#3B82F6" - } - }, - "satisfaction": { - "type": "range", - "title": "Satisfaction Level", - "order": 19, - "ui": { - "element": "input", - "type": "range", - "class": "form-group", - "name": "satisfaction", - "min": "1", - "max": "10", - "value": "5" - } - }, - "profile_picture": { - "type": "file", - "title": "Profile Picture", - "order": 20, - "ui": { - "element": "input", - "type": "file", - "class": "form-group", - "name": "profile_picture", - "accept": "image/*" - } - }, - "newsletter": { - "type": "boolean", - "title": "Subscribe to Newsletter", - "order": 21, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group", - "name": "newsletter", - "value": "yes" - } - }, - "gender": { - "type": "string", - "title": "Gender", - "order": 22, - "ui": { - "element": "select", - "class": "form-group", - "name": "gender", - "options": [ - { "value": "", "text": "Select Gender", "selected": true }, - { "value": "male", "text": "Male" }, - { "value": "female", "text": "Female" }, - { "value": "other", "text": "Other" }, - { "value": "prefer_not_to_say", "text": "Prefer not to say" } - ] - } - }, - "country": { - "type": "string", - "title": "Country", - "order": 23, - "ui": { - "element": "select", - "class": "form-group", - "name": "country", - "options": [ - { "value": "us", "text": "United States" }, - { "value": "ca", "text": "Canada" }, - { "value": "uk", "text": "United Kingdom" }, - { "value": "de", "text": "Germany" }, - { "value": "fr", "text": "France" }, - { "value": "jp", "text": "Japan" }, - { "value": "au", "text": "Australia" } - ] - } - }, - "skills": { - "type": "string", - "title": "Skills", - "order": 24, - "ui": { - "element": "select", - "class": "form-group", - "name": "skills", - "multiple": true, - "options": [ - { "value": "javascript", "text": "JavaScript" }, - { "value": "python", "text": "Python" }, - { "value": "go", "text": "Go" }, - { "value": "rust", "text": "Rust" }, - { "value": "java", "text": "Java" }, - { "value": "csharp", "text": "C#" } - ] - } - }, - "bio": { - "type": "string", - "title": "Biography", - "placeholder": "Tell us about yourself...", - "order": 25, - "ui": { - "element": "textarea", - "class": "form-group", - "name": "bio", - "rows": "4", - "cols": "50", - "maxlength": "500" - } - }, - "section_header": { - "type": "string", - "order": 30, - "ui": { - "element": "h2", - "class": "text-2xl font-semibold mt-8 mb-4", - "content": "Additional Information" - } - }, - "experience_fieldset": { - "type": "object", - "order": 31, - "ui": { - "element": "fieldset", - "class": "border border-gray-300 rounded p-4 mb-4", - "children": [ - { - "ui": { - "element": "legend", - "class": "font-medium px-2", - "content": "Work Experience" - } - }, - { - "title": "Years of Experience", - "ui": { - "element": "input", - "type": "number", - "name": "years_experience", - "class": "form-group", - "min": "0", - "max": "50" - } - }, - { - "title": "Current Position", - "ui": { - "element": "input", - "type": "text", - "name": "current_position", - "class": "form-group", - "placeholder": "e.g., Software Engineer" - } - } - ] - } - }, - "technologies_datalist": { - "type": "string", - "title": "Preferred Technology", - "order": 32, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "name": "preferred_tech", - "list": "technologies", - "placeholder": "Start typing..." - } - }, - "tech_datalist": { - "type": "string", - "order": 33, - "ui": { - "element": "datalist", - "id": "technologies", - "options": [ - "React", "Vue.js", "Angular", "Node.js", "Express", - "Django", "Flask", "Spring Boot", "ASP.NET", "Laravel" - ] - } - }, - "completion_progress": { - "type": "string", - "order": 34, - "ui": { - "element": "div", - "class": "mb-4", - "contentHTML": "70%" - } - }, - "rating_meter": { - "type": "string", - "order": 35, - "ui": { - "element": "div", - "class": "mb-4", - "contentHTML": "8 out of 10" - } - }, - "media_section": { - "type": "string", - "order": 40, - "ui": { - "element": "section", - "class": "mt-8 mb-4", - "contentHTML": "

Media Examples

" - } - }, - "demo_image": { - "type": "string", - "order": 41, - "ui": { - "element": "figure", - "class": "mb-4", - "contentHTML": "Demo placeholder image
Sample image with caption
" - } - }, - "table_section": { - "type": "string", - "order": 50, - "ui": { - "element": "div", - "class": "mt-8 mb-4", - "contentHTML": "

Data Table Example

NameRoleExperience
John DoeDeveloper5 years
Jane SmithDesigner3 years
" - } - }, - "list_examples": { - "type": "string", - "order": 60, - "ui": { - "element": "div", - "class": "mt-8 mb-4", - "contentHTML": "

List Examples

Unordered List:

Ordered List:

  1. Step one
  2. Step two
  3. Step three

Description List:

Term 1:
Definition 1
Term 2:
Definition 2
" - } - }, - "interactive_details": { - "type": "string", - "order": 70, - "ui": { - "element": "details", - "class": "border border-gray-300 rounded p-4 mb-4", - "contentHTML": "Click to expand advanced options
" - } - }, - "code_example": { - "type": "string", - "order": 80, - "ui": { - "element": "div", - "class": "mt-8 mb-4", - "contentHTML": "

Code Example

function greetUser(name) {\n    return `Hello, ${name}!`;\n}\n\nconsole.log(greetUser('World'));
" - } - }, - "text_formatting": { - "type": "string", - "order": 90, - "ui": { - "element": "div", - "class": "mt-8 mb-4", - "contentHTML": "

Text Formatting Examples

This paragraph contains various text formatting: bold text, italic text, highlighted text, small text, deleted text, inserted text, superscript, subscript, and HTML abbreviation.

This is a blockquote that can contain longer quoted text with proper styling and indentation.
Contact: demo@example.com
" - } - }, - "break_line": { - "type": "string", - "order": 95, - "ui": { - "element": "br" - } - }, - "final_divider": { - "type": "string", - "order": 96, - "ui": { - "element": "hr", - "class": "my-8 border-gray-300" - } - }, - "footer_note": { - "type": "string", - "order": 97, - "ui": { - "element": "footer", - "class": "text-center text-gray-500 text-sm", - "contentHTML": "

This comprehensive form demonstrates the full capabilities of the enhanced JSON Schema renderer.

" - } - } - }, - "required": [ - "first_name", "last_name", "email", "age", "birth_date", "gender", "bio" - ], - "form": { - "class": "max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg", - "action": "/process?task_id={{task_id}}&form=comprehensive", - "method": "POST", - "enctype": "multipart/form-data", - "groups": [ - { - "title": { - "text": "Header Section", - "class": "sr-only" - }, - "fields": [ "page_title", "intro_paragraph", "divider" ], - "class": "mb-8" - }, - { - "title": { - "text": "Personal Information", - "class": "text-2xl font-semibold mb-6 text-gray-800" - }, - "fields": [ - "first_name", "last_name", "email", "password", "age", - "birth_date", "website", "phone", "favorite_color", - "satisfaction", "profile_picture", "newsletter" - ], - "class": "grid grid-cols-1 md:grid-cols-2 gap-4 mb-8" - }, - { - "title": { - "text": "Preferences & Background", - "class": "text-2xl font-semibold mb-6 text-gray-800" - }, - "fields": [ "gender", "country", "skills", "bio" ], - "class": "space-y-4 mb-8" - }, - { - "title": { - "text": "Professional Details", - "class": "sr-only" - }, - "fields": [ - "section_header", "experience_fieldset", "technologies_datalist", - "tech_datalist", "completion_progress", "rating_meter" - ], - "class": "mb-8" - }, - { - "title": { - "text": "Media & Content Examples", - "class": "sr-only" - }, - "fields": [ "media_section", "demo_image" ], - "class": "mb-8" - }, - { - "title": { - "text": "Data & Text Examples", - "class": "sr-only" - }, - "fields": [ - "table_section", "list_examples", "interactive_details", - "code_example", "text_formatting" - ], - "class": "mb-8" - }, - { - "title": { - "text": "Footer", - "class": "sr-only" - }, - "fields": [ "break_line", "final_divider", "footer_note" ], - "class": "mt-8" - } - ], - "submit": { - "type": "submit", - "label": "Submit Complete Form", - "class": "bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4" - }, - "reset": { - "type": "reset", - "label": "Reset All Fields", - "class": "bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4" - }, - "buttons": [ - { - "type": "button", - "label": "Save Draft", - "class": "bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200", - "onclick": "saveDraft()" - } - ] - } -} diff --git a/examples/app/server.go b/examples/app/server.go deleted file mode 100644 index fb980cc..0000000 --- a/examples/app/server.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/oarkflow/mq/renderer" - - "github.com/oarkflow/form" -) - -func main() { - http.Handle("/form.css", http.FileServer(http.Dir("templates"))) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Serve the main form page - http.ServeFile(w, r, "templates/form.html") - }) - http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest) - return - } - data, err := form.DecodeForm(body) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to decode form: %v", err), http.StatusBadRequest) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(data) - }) - http.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) { - templateName := r.URL.Query().Get("template") - if templateName == "" { - templateName = "basic" - } - schemaHTML := r.URL.Query().Get("schema") - if schemaHTML == "" { - http.Error(w, "Schema parameter is required", http.StatusBadRequest) - return - } - if !strings.Contains(schemaHTML, ".json") { - schemaHTML = fmt.Sprintf("%s.json", schemaHTML) - } - renderer, err := renderer.GetFromFile(schemaHTML, templateName) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to get cached template: %v", err), http.StatusInternalServerError) - return - } - - // Set template data for dynamic interpolation - templateData := map[string]interface{}{ - "task_id": r.URL.Query().Get("task_id"), // Get task_id from query params - "session_id": "test_session_123", // Example session_id - } - // If task_id is not provided, use a default value - if templateData["task_id"] == "" { - templateData["task_id"] = "default_task_123" - } - - renderedHTML, err := renderer.RenderFields(templateData) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to render fields: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/html") - w.Write([]byte(renderedHTML)) - }) - - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - - fmt.Printf("Server running on port http://localhost:%s\n", port) - http.ListenAndServe(fmt.Sprintf(":%s", port), nil) -} diff --git a/examples/app/templates/advanced.html b/examples/app/templates/advanced.html deleted file mode 100644 index ebaf6b8..0000000 --- a/examples/app/templates/advanced.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Advanced Template - - - - - - -
-
-

Form Fields

- {{form_groups}} -
- {{form_buttons}} -
-
-
- - - diff --git a/examples/app/templates/basic.html b/examples/app/templates/basic.html deleted file mode 100644 index f4ed557..0000000 --- a/examples/app/templates/basic.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Basic Template - - - - - - -
-
- {{form_groups}} -
- {{form_buttons}} -
-
-
- - - diff --git a/examples/app/templates/form.css b/examples/app/templates/form.css deleted file mode 100644 index df6615b..0000000 --- a/examples/app/templates/form.css +++ /dev/null @@ -1,780 +0,0 @@ -/* CSS Reset and Base Styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - line-height: 1.6; - scroll-behavior: smooth; -} - -body { - font-family: 'Arial', 'Helvetica', sans-serif; - background-color: #f8f9fa; - color: #333; - margin: 0; - padding: 0; -} - -/* Typography and Text Elements */ -h1, h2, h3, h4, h5, h6 { - font-weight: bold; - line-height: 1.2; - margin-bottom: 0.5rem; - color: #2c3e50; -} - -h1 { font-size: 2.5rem; } -h2 { font-size: 2rem; } -h3 { font-size: 1.75rem; } -h4 { font-size: 1.5rem; } -h5 { font-size: 1.25rem; } -h6 { font-size: 1rem; } - -p { - margin-bottom: 1rem; - line-height: 1.6; -} - -/* Inline text elements */ -strong, b { - font-weight: bold; -} - -em, i { - font-style: italic; -} - -small { - font-size: 0.875rem; - color: #666; -} - -mark { - background-color: #fff3cd; - padding: 0.125rem 0.25rem; - border-radius: 0.125rem; -} - -del { - text-decoration: line-through; - color: #dc3545; -} - -ins { - text-decoration: underline; - color: #28a745; - background-color: #d4edda; -} - -sub, sup { - font-size: 0.75rem; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { bottom: -0.25rem; } -sup { top: -0.5rem; } - -abbr { - text-decoration: underline dotted; - cursor: help; -} - -code { - font-family: 'Courier New', monospace; - background-color: #f8f9fa; - padding: 0.125rem 0.25rem; - border-radius: 0.125rem; - font-size: 0.875rem; - color: #e83e8c; -} - -pre { - font-family: 'Courier New', monospace; - background-color: #f8f9fa; - padding: 1rem; - border-radius: 0.25rem; - overflow-x: auto; - margin-bottom: 1rem; - border: 1px solid #dee2e6; -} - -pre code { - background: none; - padding: 0; - font-size: inherit; - color: inherit; -} - -blockquote { - margin: 1rem 0; - padding: 1rem; - border-left: 4px solid #007bff; - background-color: #f8f9fa; - font-style: italic; -} - -cite { - font-style: italic; - color: #666; -} - -address { - font-style: normal; - margin-bottom: 1rem; -} - -time { - color: #666; -} - -/* Links */ -a { - color: #007bff; - text-decoration: none; - transition: color 0.2s ease; -} - -a:hover { - color: #0056b3; - text-decoration: underline; -} - -a:focus { - outline: 2px solid #007bff; - outline-offset: 2px; -} - -a:visited { - color: #6f42c1; -} - -/* Lists */ -ul, ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -ul { - list-style-type: disc; -} - -ol { - list-style-type: decimal; -} - -li { - margin-bottom: 0.25rem; -} - -ul ul, ol ol, ul ol, ol ul { - margin-bottom: 0; - margin-top: 0.25rem; -} - -/* Nested list styles */ -ul ul { list-style-type: circle; } -ul ul ul { list-style-type: square; } - -ol ol { list-style-type: lower-alpha; } -ol ol ol { list-style-type: lower-roman; } - -/* Description lists */ -dl { - margin-bottom: 1rem; -} - -dt { - font-weight: bold; - margin-top: 0.5rem; -} - -dd { - margin-left: 1rem; - margin-bottom: 0.5rem; -} - -/* Form Elements */ -.form-group { - margin-bottom: 1rem; - width: 100%; -} - -.required { - color: #dc3545; -} - -.form-group label { - display: block; - font-weight: bold; - margin-bottom: 0.5rem; - color: #333; -} - -/* Input elements */ -input[type="text"], -input[type="email"], -input[type="password"], -input[type="number"], -input[type="tel"], -input[type="url"], -input[type="search"], -input[type="date"], -input[type="time"], -input[type="datetime-local"], -input[type="month"], -input[type="week"], -input[type="color"], -input[type="file"], -select, -textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid #ccc; - border-radius: 0.25rem; - font-size: 1rem; - font-family: inherit; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -/* Ensure form-group input styles apply to all types including password */ -.form-group input[type="password"], .form-group select { - width: 100%; - padding: 0.75rem; - border: 1px solid #ccc; - border-radius: 0.25rem; - font-size: 1rem; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); -} - -input:focus, -select:focus, -textarea:focus { - border-color: #007bff; - outline: none; - box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); -} - -/* Specific focus styles for form-group inputs */ -.form-group input[type="password"]:focus, -.form-group input[type="text"]:focus, -.form-group input[type="email"]:focus, -.form-group input[type="number"]:focus, -.form-group select:focus, -.form-group textarea:focus { - border-color: #007bff; - outline: none; - box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); -} - -input:invalid { - border-color: #dc3545; -} - -input:invalid:focus { - box-shadow: 0 0 5px rgba(220, 53, 69, 0.5); -} - -/* Range input */ -input[type="range"] { - width: 100%; - height: 0.5rem; - background: #ddd; - border-radius: 0.25rem; - outline: none; - padding: 0; - box-shadow: none; -} - -input[type="range"]::-webkit-slider-thumb { - appearance: none; - width: 1.5rem; - height: 1.5rem; - background: #007bff; - border-radius: 50%; - cursor: pointer; -} - -input[type="range"]::-moz-range-thumb { - width: 1.5rem; - height: 1.5rem; - background: #007bff; - border-radius: 50%; - cursor: pointer; - border: none; -} - -/* Checkbox and radio */ -input[type="checkbox"], -input[type="radio"] { - width: auto; - margin-right: 0.5rem; - padding: 0; - box-shadow: none; -} - -input[type="checkbox"] { - border-radius: 0.125rem; -} - -input[type="radio"] { - border-radius: 50%; -} - -/* Select styling */ -select { - appearance: none; - background: url('data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns%3D%22http%3A//www.w3.org/2000/svg%22 viewBox%3D%220 0 4 5%22%3E%3Cpath fill%3D%22%23000%22 d%3D%22M2 0L0 2h4z%22/%3E%3C/svg%3E') no-repeat right 0.75rem center; - background-size: 0.5rem; - padding-right: 2.5rem; -} - -select[multiple] { - background-image: none; - padding-right: 0.75rem; - height: auto; - min-height: 6rem; -} - -optgroup { - font-weight: bold; - color: #666; -} - -option { - padding: 0.25rem; -} - -/* Textarea */ -textarea { - resize: vertical; - min-height: 6rem; -} - -/* Fieldset and Legend */ -fieldset { - border: 1px solid #ccc; - border-radius: 0.25rem; - padding: 1rem; - margin-bottom: 1rem; -} - -legend { - font-weight: bold; - padding: 0 0.5rem; - color: #333; -} - -/* Progress and Meter */ -progress, meter { - width: 100%; - height: 1.5rem; - appearance: none; - border: none; - border-radius: 0.25rem; - background-color: #e9ecef; -} - -progress::-webkit-progress-bar, -meter::-webkit-meter-bar { - background-color: #e9ecef; - border-radius: 0.25rem; -} - -progress::-webkit-progress-value { - background-color: #007bff; - border-radius: 0.25rem; -} - -meter::-webkit-meter-optimum-value { - background-color: #28a745; -} - -meter::-webkit-meter-suboptimum-value { - background-color: #ffc107; -} - -meter::-webkit-meter-even-less-good-value { - background-color: #dc3545; -} - -/* Output */ -output { - display: inline-block; - padding: 0.375rem 0.75rem; - background-color: #f8f9fa; - border: 1px solid #ced4da; - border-radius: 0.25rem; -} - -/* Datalist styling (limited browser support) */ -datalist { - display: none; -} - -/* Buttons */ -button, -input[type="button"], -input[type="submit"], -input[type="reset"] { - display: inline-block; - padding: 0.75rem 1.5rem; - font-size: 1rem; - font-weight: bold; - font-family: inherit; - border: none; - border-radius: 0.25rem; - cursor: pointer; - text-align: center; - text-decoration: none; - transition: all 0.2s ease; - background-color: #6c757d; - color: white; -} - -button:hover, -input[type="button"]:hover, -input[type="submit"]:hover, -input[type="reset"]:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -button:active, -input[type="button"]:active, -input[type="submit"]:active, -input[type="reset"]:active { - transform: translateY(0); -} - -button:disabled, -input[type="button"]:disabled, -input[type="submit"]:disabled, -input[type="reset"]:disabled { - background-color: #ccc; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -button:focus, -input[type="button"]:focus, -input[type="submit"]:focus, -input[type="reset"]:focus { - outline: 2px solid #007bff; - outline-offset: 2px; -} - -/* Button variants */ -.btn-primary { - background-color: #007bff; - color: white; -} - -.btn-primary:hover { - background-color: #0056b3; -} - -.btn-primary:active { - background-color: #004085; -} - -.btn-secondary { - background-color: #6c757d; - color: white; -} - -.btn-secondary:hover { - background-color: #5a6268; -} - -.btn-secondary:active { - background-color: #4e555b; -} - -.btn-success { - background-color: #28a745; - color: white; -} - -.btn-success:hover { - background-color: #218838; -} - -.btn-danger { - background-color: #dc3545; - color: white; -} - -.btn-danger:hover { - background-color: #c82333; -} - -.btn-warning { - background-color: #ffc107; - color: #212529; -} - -.btn-warning:hover { - background-color: #e0a800; -} - -.btn-info { - background-color: #17a2b8; - color: white; -} - -.btn-info:hover { - background-color: #138496; -} - -.btn-light { - background-color: #f8f9fa; - color: #212529; - border: 1px solid #dee2e6; -} - -.btn-light:hover { - background-color: #e2e6ea; -} - -.btn-dark { - background-color: #343a40; - color: white; -} - -.btn-dark:hover { - background-color: #23272b; -} - -/* Tables */ -table { - width: 100%; - border-collapse: collapse; - margin-bottom: 1rem; - background-color: white; -} - -th, td { - padding: 0.75rem; - text-align: left; - border-bottom: 1px solid #dee2e6; - vertical-align: top; -} - -th { - font-weight: bold; - background-color: #f8f9fa; - border-top: 1px solid #dee2e6; -} - -tr:hover { - background-color: #f5f5f5; -} - -caption { - font-weight: bold; - margin-bottom: 0.5rem; - color: #666; -} - -thead th { - vertical-align: bottom; - border-bottom: 2px solid #dee2e6; -} - -tbody + tbody { - border-top: 2px solid #dee2e6; -} - -/* Table variants */ -.table-striped tbody tr:nth-of-type(odd) { - background-color: #f9f9f9; -} - -.table-bordered { - border: 1px solid #dee2e6; -} - -.table-bordered th, -.table-bordered td { - border: 1px solid #dee2e6; -} - -/* Images and Media */ -img { - max-width: 100%; - height: auto; - border-radius: 0.25rem; -} - -figure { - margin: 1rem 0; - text-align: center; -} - -figcaption { - font-size: 0.875rem; - color: #666; - margin-top: 0.5rem; - font-style: italic; -} - -audio, video { - width: 100%; - max-width: 100%; -} - -canvas { - max-width: 100%; - height: auto; -} - -svg { - max-width: 100%; - height: auto; - fill: currentColor; -} - -/* Sectioning Elements */ -article, section, nav, aside { - margin-bottom: 1rem; -} - -header, footer { - padding: 1rem 0; -} - -main { - min-height: calc(100vh - 200px); -} - -/* Interactive Elements */ -details { - margin-bottom: 1rem; - border: 1px solid #dee2e6; - border-radius: 0.25rem; - padding: 1rem; -} - -summary { - font-weight: bold; - cursor: pointer; - margin-bottom: 0.5rem; - padding: 0.5rem; - background-color: #f8f9fa; - border-radius: 0.25rem; - transition: background-color 0.2s ease; -} - -summary:hover { - background-color: #e9ecef; -} - -details[open] summary { - margin-bottom: 1rem; -} - -dialog { - border: 1px solid #ccc; - border-radius: 0.5rem; - padding: 1rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - background: white; -} - -dialog::backdrop { - background: rgba(0, 0, 0, 0.5); -} - -/* Embedded Content */ -iframe { - width: 100%; - border: none; - border-radius: 0.25rem; -} - -embed, object { - max-width: 100%; -} - -/* Custom Layout Utilities (Non-Tailwind) */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; -} - -.row { - display: flex; - flex-wrap: wrap; - margin: -0.5rem; -} - -.col { - flex: 1; - padding: 0.5rem; -} - -/* Error states */ -.form-control-error { - color: #dc3545; - font-size: 0.875rem; - margin-top: 0.25rem; -} - -.is-invalid { - border-color: #dc3545; -} - -.is-valid { - border-color: #28a745; -} - -/* Print styles */ -@media print { - * { - box-shadow: none !important; - text-shadow: none !important; - } - - a, a:visited { - text-decoration: underline; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - pre, blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; - } - - tr, img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - p, h2, h3 { - orphans: 3; - widows: 3; - } - - h2, h3 { - page-break-after: avoid; - } -} diff --git a/examples/app/validation-features.json b/examples/app/validation-features.json deleted file mode 100644 index 81dcc9e..0000000 --- a/examples/app/validation-features.json +++ /dev/null @@ -1,586 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Advanced Validation Features Demo", - "description": "Demonstrates all implemented validation features", - "properties": { - "stringValidations": { - "type": "object", - "title": "String Validation Examples", - "properties": { - "basicString": { - "type": "string", - "title": "Basic String with Length Limits", - "minLength": 3, - "maxLength": 20, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 1 - } - }, - "patternString": { - "type": "string", - "title": "Pattern Validation (Letters Only)", - "pattern": "^[A-Za-z\\s]+$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "order": 2, - "placeholder": "Letters and spaces only" - } - }, - "emailField": { - "type": "string", - "format": "email", - "title": "Email Format Validation", - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "order": 3 - } - }, - "urlField": { - "type": "string", - "format": "uri", - "title": "URL Format Validation", - "ui": { - "element": "input", - "type": "url", - "class": "form-group", - "order": 4 - } - }, - "dateField": { - "type": "string", - "format": "date", - "title": "Date Format Validation", - "ui": { - "element": "input", - "type": "date", - "class": "form-group", - "order": 5 - } - }, - "enumField": { - "type": "string", - "title": "Enum Selection", - "enum": [ "option1", "option2", "option3" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 6, - "options": [ - { "value": "option1", "text": "Option 1" }, - { "value": "option2", "text": "Option 2" }, - { "value": "option3", "text": "Option 3" } - ] - } - } - }, - "required": [ "basicString", "emailField" ] - }, - "numericValidations": { - "type": "object", - "title": "Numeric Validation Examples", - "properties": { - "integerField": { - "type": "integer", - "title": "Integer with Range", - "minimum": 1, - "maximum": 100, - "ui": { - "element": "input", - "type": "number", - "class": "form-group", - "order": 1 - } - }, - "numberField": { - "type": "number", - "title": "Number with Exclusive Range", - "exclusiveMinimum": 0, - "exclusiveMaximum": 1000, - "multipleOf": 0.5, - "ui": { - "element": "input", - "type": "number", - "class": "form-group", - "order": 2, - "step": "0.5" - } - }, - "rangeField": { - "type": "integer", - "title": "Range Slider", - "minimum": 0, - "maximum": 100, - "default": 50, - "ui": { - "element": "input", - "type": "range", - "class": "form-group", - "order": 3 - } - } - } - }, - "arrayValidations": { - "type": "object", - "title": "Array Validation Examples", - "properties": { - "multiSelect": { - "type": "array", - "title": "Multi-Select with Constraints", - "minItems": 2, - "maxItems": 4, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ "red", "green", "blue", "yellow", "purple", "orange" ] - }, - "ui": { - "element": "select", - "class": "form-group", - "order": 1, - "multiple": true, - "options": [ - { "value": "red", "text": "Red" }, - { "value": "green", "text": "Green" }, - { "value": "blue", "text": "Blue" }, - { "value": "yellow", "text": "Yellow" }, - { "value": "purple", "text": "Purple" }, - { "value": "orange", "text": "Orange" } - ] - } - }, - "dynamicList": { - "type": "array", - "title": "Dynamic List of Objects", - "minItems": 1, - "maxItems": 5, - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name", - "minLength": 2 - }, - "value": { - "type": "number", - "title": "Value", - "minimum": 0 - } - }, - "required": [ "name" ] - }, - "ui": { - "element": "fieldset", - "class": "dynamic-list", - "order": 2 - } - } - } - }, - "conditionalValidations": { - "type": "object", - "title": "Conditional Validation Examples", - "properties": { - "userType": { - "type": "string", - "title": "User Type", - "enum": [ "individual", "business", "non-profit" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 1, - "options": [ - { "value": "individual", "text": "Individual" }, - { "value": "business", "text": "Business" }, - { "value": "non-profit", "text": "Non-Profit" } - ] - } - }, - "individualInfo": { - "type": "object", - "title": "Individual Information", - "properties": { - "ssn": { - "type": "string", - "title": "Social Security Number", - "pattern": "^\\d{3}-\\d{2}-\\d{4}$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "placeholder": "XXX-XX-XXXX" - } - } - } - }, - "businessInfo": { - "type": "object", - "title": "Business Information", - "properties": { - "taxId": { - "type": "string", - "title": "Tax ID", - "pattern": "^\\d{2}-\\d{7}$", - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "placeholder": "XX-XXXXXXX" - } - }, - "businessName": { - "type": "string", - "title": "Business Name", - "minLength": 2, - "ui": { - "element": "input", - "type": "text", - "class": "form-group" - } - } - } - } - }, - "allOf": [ - { - "if": { - "properties": { - "userType": { "const": "individual" } - } - }, - "then": { - "properties": { - "individualInfo": { - "required": [ "ssn" ] - } - } - } - }, - { - "if": { - "properties": { - "userType": { "const": "business" } - } - }, - "then": { - "properties": { - "businessInfo": { - "required": [ "taxId", "businessName" ] - } - } - } - } - ] - }, - "dependentFields": { - "type": "object", - "title": "Dependent Field Examples", - "properties": { - "hasPhone": { - "type": "boolean", - "title": "I have a phone number", - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group", - "order": 1 - } - }, - "phoneNumber": { - "type": "string", - "title": "Phone Number", - "pattern": "^\\+?[1-9]\\d{1,14}$", - "ui": { - "element": "input", - "type": "tel", - "class": "form-group", - "order": 2 - } - }, - "preferredContact": { - "type": "string", - "title": "Preferred Contact Method", - "enum": [ "email", "phone", "mail" ], - "ui": { - "element": "select", - "class": "form-group", - "order": 3, - "options": [ - { "value": "email", "text": "Email" }, - { "value": "phone", "text": "Phone" }, - { "value": "mail", "text": "Physical Mail" } - ] - } - }, - "mailingAddress": { - "type": "string", - "title": "Mailing Address", - "ui": { - "element": "textarea", - "class": "form-group", - "order": 4, - "rows": 3 - } - } - }, - "dependentRequired": { - "hasPhone": [ "phoneNumber" ], - "preferredContact": [ "preferredContact" ] - }, - "if": { - "properties": { - "preferredContact": { "const": "phone" } - } - }, - "then": { - "required": [ "phoneNumber" ] - }, - "else": { - "if": { - "properties": { - "preferredContact": { "const": "mail" } - } - }, - "then": { - "required": [ "mailingAddress" ] - } - } - }, - "compositionValidations": { - "type": "object", - "title": "Composition Validation Examples (allOf, anyOf, oneOf)", - "properties": { - "contactMethod": { - "title": "Contact Method Validation", - "anyOf": [ - { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "Email Address" - } - }, - "required": [ "email" ] - }, - { - "type": "object", - "properties": { - "phone": { - "type": "string", - "pattern": "^\\+?[1-9]\\d{1,14}$", - "title": "Phone Number" - } - }, - "required": [ "phone" ] - } - ], - "ui": { - "element": "fieldset", - "class": "contact-methods", - "order": 1 - } - }, - "paymentMethod": { - "title": "Payment Method (One Of)", - "oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "credit_card" }, - "cardNumber": { - "type": "string", - "pattern": "^\\d{16}$", - "title": "Card Number" - }, - "expiryDate": { - "type": "string", - "pattern": "^(0[1-9]|1[0-2])\\/\\d{2}$", - "title": "Expiry Date (MM/YY)" - } - }, - "required": [ "type", "cardNumber", "expiryDate" ] - }, - { - "type": "object", - "properties": { - "type": { "const": "paypal" }, - "paypalEmail": { - "type": "string", - "format": "email", - "title": "PayPal Email" - } - }, - "required": [ "type", "paypalEmail" ] - } - ], - "ui": { - "element": "fieldset", - "class": "payment-methods", - "order": 2 - } - } - } - }, - "advancedFeatures": { - "type": "object", - "title": "Advanced Features", - "properties": { - "constField": { - "const": "REQUIRED_VALUE", - "title": "Constant Value Field", - "ui": { - "element": "input", - "type": "hidden", - "value": "REQUIRED_VALUE" - } - }, - "booleanField": { - "type": "boolean", - "title": "Boolean Checkbox", - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-group" - } - }, - "readOnlyField": { - "type": "string", - "title": "Read Only Field", - "default": "This is read only", - "readOnly": true, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "readonly": true - } - }, - "textareaField": { - "type": "string", - "title": "Large Text Area", - "maxLength": 500, - "ui": { - "element": "textarea", - "class": "form-group", - "rows": 4, - "placeholder": "Enter detailed information here..." - } - } - } - } - }, - "required": [ "stringValidations" ], - "form": { - "class": "validation-demo-form", - "action": "/api/validate-form", - "method": "POST", - "groups": [ - { - "title": { - "text": "String Validations", - "class": "section-header" - }, - "class": "string-validation-section", - "fields": [ - "stringValidations.basicString", - "stringValidations.patternString", - "stringValidations.emailField", - "stringValidations.urlField", - "stringValidations.dateField", - "stringValidations.enumField" - ] - }, - { - "title": { - "text": "Numeric Validations", - "class": "section-header" - }, - "class": "numeric-validation-section", - "fields": [ - "numericValidations.integerField", - "numericValidations.numberField", - "numericValidations.rangeField" - ] - }, - { - "title": { - "text": "Array Validations", - "class": "section-header" - }, - "class": "array-validation-section", - "fields": [ - "arrayValidations.multiSelect", - "arrayValidations.dynamicList" - ] - }, - { - "title": { - "text": "Conditional Validations", - "class": "section-header" - }, - "class": "conditional-validation-section", - "fields": [ - "conditionalValidations.userType", - "conditionalValidations.individualInfo", - "conditionalValidations.businessInfo" - ] - }, - { - "title": { - "text": "Dependent Fields", - "class": "section-header" - }, - "class": "dependent-validation-section", - "fields": [ - "dependentFields.hasPhone", - "dependentFields.phoneNumber", - "dependentFields.preferredContact", - "dependentFields.mailingAddress" - ] - }, - { - "title": { - "text": "Composition Validations", - "class": "section-header" - }, - "class": "composition-validation-section", - "fields": [ - "compositionValidations.contactMethod", - "compositionValidations.paymentMethod" - ] - }, - { - "title": { - "text": "Advanced Features", - "class": "section-header" - }, - "class": "advanced-features-section", - "fields": [ - "advancedFeatures.constField", - "advancedFeatures.booleanField", - "advancedFeatures.readOnlyField", - "advancedFeatures.textareaField" - ] - } - ], - "submit": { - "label": "Validate Form", - "class": "btn btn-primary" - }, - "reset": { - "label": "Reset All", - "class": "btn btn-secondary" - } - } -} diff --git a/examples/certs/ca.crt b/examples/certs/ca.crt deleted file mode 100644 index c366a6e..0000000 --- a/examples/certs/ca.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIQBuz6Swcf+c/9yR95gWHeMzANBgkqhkiG9w0BAQsFADBu -MQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMRAwDgYDVQQHEwdNeSBDaXR5MRIwEAYD -VQQJEwlNeSBTdHJlZXQxDjAMBgNVBBETBTAwMDAwMQ4wDAYDVQQKEwVNeSBDQTEO -MAwGA1UEAxMFTXkgQ0EwHhcNMjQxMDAxMDU0MzE2WhcNMjUxMDAxMDU0MzE2WjBu -MQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMRAwDgYDVQQHEwdNeSBDaXR5MRIwEAYD -VQQJEwlNeSBTdHJlZXQxDjAMBgNVBBETBTAwMDAwMQ4wDAYDVQQKEwVNeSBDQTEO -MAwGA1UEAxMFTXkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD -hr0IwUOXUWzfZOdxjMEGxbp7q5jGzeC0VmjCRmvvWQgV4aX+pCYSEzDBwe+2ryFx -Kpvp75/SDTTT93bC3/wYSS0XpcJoISoSb3qVhcoQXGB7d80tPfyZO+MCPOCtnJf2 -2mRT1uN79tA2gsMLYboatTQVQ1HJZQbs7h+HQQsm2PBdIRBwZGRlh90eFzF/BUIj -rJq9Dg0xmI1lmKXW/XuUR8P8rwMf0CZSpKYAvf4P6ORF8oJlhfuj5im+eH2pYkGZ -kpA7I2+1wk8UJSIXXR4XMkd9PsByHjeRWLBDViFaiKPqf/XdHda56lxhnOQGTidU -cGud/8MsWJj5c9hdBDPPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAPBgNVHRMB -Af8EBTADAQH/MB0GA1UdDgQWBBTiLIKo+Kb2S1OK6Ch3p3+ZghZfEzANBgkqhkiG -9w0BAQsFAAOCAQEAN49h/WETvhyCuLTWFgl+dGK+uM0k5yRwTFtThu7FLZdqOvis -l4WthkLt3oanNSyxs4RhDvd6oZ6lQvY7lNy4Z6U83QYR1O7dTVPb7wWrsnjUd4/l -be3qfoIo5SMPr3db0D019LI+vq5UUk4DC0YpI/DPFLL9kfDHvdtlRbIrmgbEjwwN -smu2wcbSNM22yk2P5vFE9jgRLZ4rQYpvMPrnTEACr6uLdVut6rpx2PpVq6W0vNK4 -4c2PGNxXolTlZBmz7knih3WJrgOuoaeVIN8WqR9GiZQjFPzr4AFM31q5BNUlK4TU -Xi1a0yAjs0xWu0NCq1zbwh9+SB8SuYtIvsjILQ== ------END CERTIFICATE----- diff --git a/examples/certs/ca.key b/examples/certs/ca.key deleted file mode 100644 index 4a61a2f..0000000 --- a/examples/certs/ca.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAw4a9CMFDl1Fs32TncYzBBsW6e6uYxs3gtFZowkZr71kIFeGl -/qQmEhMwwcHvtq8hcSqb6e+f0g000/d2wt/8GEktF6XCaCEqEm96lYXKEFxge3fN -LT38mTvjAjzgrZyX9tpkU9bje/bQNoLDC2G6GrU0FUNRyWUG7O4fh0ELJtjwXSEQ -cGRkZYfdHhcxfwVCI6yavQ4NMZiNZZil1v17lEfD/K8DH9AmUqSmAL3+D+jkRfKC -ZYX7o+Ypvnh9qWJBmZKQOyNvtcJPFCUiF10eFzJHfT7Ach43kViwQ1YhWoij6n/1 -3R3WuepcYZzkBk4nVHBrnf/DLFiY+XPYXQQzzwIDAQABAoIBAAkE2w1fVM3TDLGV -RvO+6Vx1nG9979Mjxfyri7OCahIlSjEwMmb3jWYCCpq1ZmhH1cQRkhWNXxLiVxB7 -9rdwe4FnRrQzii8hcH5fNAlXnYV5rV2kngs7M76hu4vr4PVBJuVVF5GidOXP8bTB -/Vs2C86Vkyxz6X7fsR0Wss+bWXdWL8RRTrFqPlHRjaaCcOK6dDzFRafvBN1CzaJ0 -Acv03BZITVSgjTfg2kNhwyrrq6EqExxLAxXrY/3cSI+bDx68W0LxVgNTfjceY5uP -uJhnYO6riuSaSD4uz3SKUUXH6zR5RDUGAPnICgM5MEDtAA3ZY3jGfbu9cJYkx9oL -rB6+1nECgYEA39PKyhHo++/wu46P94RpLZ1+RoKiE94DoZjdw9gRa4QGJUO7KGJ+ -Utnp+KzoeB3FSaXJyGj/1cLrmccsdRdSs6l4NAihQRDM8kB+7TEUCFoNfCRDQZ+J -Pf+afv/v+jR0WdlguDhFmMnAN3euJXAuHwOaZseeIRTXu8RDPkDE6qcCgYEA36GM -5Szc0TVTbcVhK3vPEBn/CgcLyjfrDbyAQp+Y0InUaQnrpzcW37sejsiK3rVyFTeQ -WQdobm6MGT+QkO3nE1/riJmyEUq3/WRYtRnGjurq6EwYeG6QxXV6SqkA9OXZSqKX -aFfH6lUs+6m4drPPzzmNEBSxt3esZGDXq29pGpkCgYByr2Z822hxjqPetlF2FdZ+ -lPAa2NyLKXra1iTrME7ctC0h8u525uCrOxTzYkVLJpXsApK9qW9M7C8kADX7WRP7 -Ep6QqstVN3KLvhhLGJaXIO0/6qS7fy8nIUzcPe+MWEw1rXgtbEfc3aMryJrme/Bl -28bFWwrfEHrprsp1n2JGiQKBgDgROdDveYFePEeGOAF97gEcc2vhLlyJvn3YJ9QM -TXTjSYT4PsPStQJs2JF1yBNkLHETWDZp/A3L24YtAKLFcqzR3KyH1DQvpod6FB97 -keOdFD4fbfcryVIoTPvQ+XNs+RiUQR+g+ndO2ZNTDvN7y3sp86r3dUMJVwhnm0rZ -COHpAoGBAJf7y/PM4T5xeBZ4LhHzGIIWaBJRRo9JkWCyLkQBVNpDs9chK6CPUcR4 -61ouAGAIRRUJt4KTJAuyt+rnEvvCTGkpq2MtrMx5+SLmj4Soh9Fo+FTqkVA15Kr/ -+05wVMIWkVqKXGF7Um9duPRUpRHhGSML3uG6rujSQWHJqibooyje ------END RSA PRIVATE KEY----- diff --git a/examples/certs/server.crt b/examples/certs/server.crt deleted file mode 100644 index cfd7865..0000000 --- a/examples/certs/server.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDUjCCAjqgAwIBAgIRAMdsW2PjhMRI5lI74hlnjacwDQYJKoZIhvcNAQELBQAw -bjELMAkGA1UEBhMCVVMxCTAHBgNVBAgTADEQMA4GA1UEBxMHTXkgQ2l0eTESMBAG -A1UECRMJTXkgU3RyZWV0MQ4wDAYDVQQREwUwMDAwMDEOMAwGA1UEChMFTXkgQ0Ex -DjAMBgNVBAMTBU15IENBMB4XDTI0MTAwMTA1NDMxNloXDTI1MTAwMTA1NDMxNlow -ETEPMA0GA1UEAxMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAxMrkyIFnEaGoZ9Z5iVVdSzoq8FTrJE3iGCZYjLRLTO/1Hq6L5C6tDqzYq3fv -V64G3B6yuWYE1SpqJ5C8T9G89Gc1jp6ZklP92nL+S7hOPjWsm+y33vM4WQQzqmY7 -BucE4yMXVZSAkr4uCe9/iTIeUBYgDOPmoJRwOS+y9mlBi6gqoWre3NDHbt9h4zim -Hg2Nsd6HT0kKcSKhrr3Xz87o8pWHyi/O7hexB3WBLIjgX43Wh0jhxwZ84FVHyCH3 -VR1UuhrInUxrWBE2HF9hhRp/8RUMgPggYIXDTNycUBJy0PEjBHy1s1hIqX75tEfP -JHNQj0NCHJ7UPFf7x1GsKPF62QIDAQABo0gwRjAOBgNVHQ8BAf8EBAMCBaAwEwYD -VR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU4iyCqPim9ktTiugod6d/mYIW -XxMwDQYJKoZIhvcNAQELBQADggEBACdY5KqLAXNHoZDof02daC+veHMM09VhFBrZ -UugYnXh6xmP+cKiINKfylr3Vqdtt4JXXMR8teTdv/Dk5ho17XtWyHbQ22bZN2DwH -vCgs4pPyqZvwfuBuWJ85fiu7B0AsrInSmdxMJeBIenTWyWU4bg19NsTejfJKIhk9 -dkvTLWryCZpoaA8lQZ+l39p10/L2PPnOdNU+TOzsbrJKnZkwCdlkAvZhyaVzTQfk -YSjVr1cakmq5T9u/8kWeb3Bx1z0GVXy0Jbgr1XBUv88IuGYH/KnrEfCweN5B+KQ7 -zwGD9PPu7E7+TpFkRH5uzv+3y8C6bICetlhWnWhQ237IEaoRpfU= ------END CERTIFICATE----- diff --git a/examples/certs/server.key b/examples/certs/server.key deleted file mode 100644 index d1e01d4..0000000 --- a/examples/certs/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAxMrkyIFnEaGoZ9Z5iVVdSzoq8FTrJE3iGCZYjLRLTO/1Hq6L -5C6tDqzYq3fvV64G3B6yuWYE1SpqJ5C8T9G89Gc1jp6ZklP92nL+S7hOPjWsm+y3 -3vM4WQQzqmY7BucE4yMXVZSAkr4uCe9/iTIeUBYgDOPmoJRwOS+y9mlBi6gqoWre -3NDHbt9h4zimHg2Nsd6HT0kKcSKhrr3Xz87o8pWHyi/O7hexB3WBLIjgX43Wh0jh -xwZ84FVHyCH3VR1UuhrInUxrWBE2HF9hhRp/8RUMgPggYIXDTNycUBJy0PEjBHy1 -s1hIqX75tEfPJHNQj0NCHJ7UPFf7x1GsKPF62QIDAQABAoIBAESksSDvYlBYHzH5 -MfOhfyVaaNfkBxFmyVK7LXAHA60Wll3ZbJpvXZYc3IcTEr12ypXFb3oUB+ODI/wh -FE6TTmHCDoBs+gx8l7O3INSwuToh5s+MxqZSGHmUaaEqf7RsqNvBxcXoQuDszYpR -rB7jCIfO7+cPJ8cjf/Gyna4uENrxdQgy8ESDYiLif5RhXZtYcuJ1dlojAshrzgz+ -PhFN7TeXTYNAit9txVybHt6m7hCzkmGjyUAhIrNoeIxizIu4osu/IoRrwhCuBRSw -3zqvOErr7JG2gI8Wj5Bs9Mkwn9iJeB4tZzGAwmw2t2eqqfdVNFFUNHE7vVRZ03/T -t/DwjLECgYEA1c79kQjzhwbuccPEOpBwl6hBngbbNw5+PtqBtizo12T3b2LsOqlZ -eb2G+5yUFW2enrZIl+KS6iCrutW1wHbxIxYijy13hlp/ecmLJeDRRdseOEagh8Re -NticHiNjluTns/sBl56unpm2lw+dp75cm+R1IiRkyiOdPcdvsfaPUZsCgYEA66BN -Z7mS6HL/juMEO5IGVluVMpIwq8seVWG8s6vQF1COlYfC/tWwxM/+95bKI/SmCMTK -83CtDrvGQ6dax2QjsxeP9mJ1GKIwEGrFrPn+vsmh0KBChG9HxVq2miWicH32WA+8 -MDwVFQUQa0MI1LsSrZsqhBGq+3KkKuBPuejTVpsCgYASnzCejTUIsaXa6r4Qi7wC -uXjdlqNJLE36k3VwtICjIfwbC3aftVhBriwvhfev1hhWonG4KNe65JWQdEScOr/N -2oOwDLm4TfGEXfVsmyQe/XKoXB5nNMcv57XROivWXKGBn38IAZ4b2i95ALcugPn3 -6fH5w0m0AV4Un2YvDdZ1uQKBgH2kUuIWYDG28HK+tskVCnAOEbaPoYhZnOkmXrrn -yORFvmIZrG66f7HSv0BClbMqh0ZxuU6qLH2IvyXgHVXpHegnjkpxIcNq6Ho4lQOx -opcVaUWXzyBTPlAMGQaFPuMBJ9S5Pz3xK8SzmJe5fQICZulPrhISYbwG22dJiPm3 -Hso1AoGAUqBG8FEyw6qQ+M3wg1n79QAWKQO8cJft7MU5ruTTJEZzm8kSqMzJ8UPz -C/CQiCehFjKNflYUZQV6RNWJp6H2vhe2qprDV88j6fhKfFi4va3CsELAKE/tAWs/ -WqydAe/dOQ8HwJCBrC4vRds8KOQTxPJUhPm/eM0Jf2Zocs5Axdc= ------END RSA PRIVATE KEY----- diff --git a/examples/clean_dag_demo.go b/examples/clean_dag_demo.go deleted file mode 100644 index af91ce3..0000000 --- a/examples/clean_dag_demo.go +++ /dev/null @@ -1,683 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" -) - -// StartProcessor - Initial node that receives and processes the input data -type StartProcessor struct { - dag.Operation -} - -func (p *StartProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("[START] Processing task %s - Initial data processing\n", task.ID) - - // Simulate initial processing work - time.Sleep(100 * time.Millisecond) - - // Create detailed result with node-specific information - processingResult := map[string]interface{}{ - "node_name": "start", - "task_id": task.ID, - "processed_at": time.Now().Format("15:04:05"), - "duration_ms": 100, - "status": "success", - "action": "initialized_data", - "data_size": len(fmt.Sprintf("%v", task.Payload)), - } - - // Initialize or update node results in context - var nodeResults map[string]interface{} - if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok { - nodeResults = existing - } else { - nodeResults = make(map[string]interface{}) - } - nodeResults["start"] = processingResult - - // Create new context with updated results - newCtx := context.WithValue(ctx, "nodeResults", nodeResults) - - fmt.Printf("[START] Node completed - data initialized for task %s\n", task.ID) - - return mq.Result{ - TaskID: task.ID, - Status: mq.Completed, - Payload: task.Payload, - Ctx: newCtx, - } -} - -// ProcessorNode - Processes and transforms the data -type ProcessorNode struct { - dag.Operation -} - -func (p *ProcessorNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("[PROCESS] Processing task %s - Data transformation\n", task.ID) - - // Simulate processing work - time.Sleep(100 * time.Millisecond) - - processingResult := map[string]interface{}{ - "node_name": "process", - "task_id": task.ID, - "processed_at": time.Now().Format("15:04:05"), - "duration_ms": 100, - "status": "success", - "action": "transformed_data", - "data_size": len(fmt.Sprintf("%v", task.Payload)), - } - - // Update node results in context - var nodeResults map[string]interface{} - if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok { - nodeResults = existing - } else { - nodeResults = make(map[string]interface{}) - } - nodeResults["process"] = processingResult - - newCtx := context.WithValue(ctx, "nodeResults", nodeResults) - - fmt.Printf("[PROCESS] Node completed - data transformed for task %s\n", task.ID) - - return mq.Result{ - TaskID: task.ID, - Status: mq.Completed, - Payload: task.Payload, - Ctx: newCtx, - } -} - -// ValidatorNode - Validates the processed data -type ValidatorNode struct { - dag.Operation -} - -func (p *ValidatorNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("[VALIDATE] Processing task %s - Data validation\n", task.ID) - - // Simulate validation work - time.Sleep(100 * time.Millisecond) - - processingResult := map[string]interface{}{ - "node_name": "validate", - "task_id": task.ID, - "processed_at": time.Now().Format("15:04:05"), - "duration_ms": 100, - "status": "success", - "action": "validated_data", - "validation": "passed", - "data_size": len(fmt.Sprintf("%v", task.Payload)), - } - - // Update node results in context - var nodeResults map[string]interface{} - if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok { - nodeResults = existing - } else { - nodeResults = make(map[string]interface{}) - } - nodeResults["validate"] = processingResult - - newCtx := context.WithValue(ctx, "nodeResults", nodeResults) - - fmt.Printf("[VALIDATE] Node completed - data validated for task %s\n", task.ID) - - return mq.Result{ - TaskID: task.ID, - Status: mq.Completed, - Payload: task.Payload, - Ctx: newCtx, - } -} - -// EndProcessor - Final node that completes the processing -type EndProcessor struct { - dag.Operation -} - -func (p *EndProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("[END] Processing task %s - Final processing\n", task.ID) - - // Simulate final processing work - time.Sleep(100 * time.Millisecond) - - processingResult := map[string]interface{}{ - "node_name": "end", - "task_id": task.ID, - "processed_at": time.Now().Format("15:04:05"), - "duration_ms": 100, - "status": "success", - "action": "finalized_data", - "data_size": len(fmt.Sprintf("%v", task.Payload)), - } - - // Update node results in context - var nodeResults map[string]interface{} - if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok { - nodeResults = existing - } else { - nodeResults = make(map[string]interface{}) - } - nodeResults["end"] = processingResult - - newCtx := context.WithValue(ctx, "nodeResults", nodeResults) - - fmt.Printf("[END] Node completed - processing finished for task %s\n", task.ID) - - return mq.Result{ - TaskID: task.ID, - Status: mq.Completed, - Payload: task.Payload, - Ctx: newCtx, - } -} - -func main() { - // Create a new DAG with enhanced features - d := dag.NewDAG("enhanced-example", "example", finalResultCallback) - - // Build the DAG structure (avoiding cycles) - buildDAG(d) - - fmt.Println("DAG validation passed! (cycle-free structure)") - - // Set up basic API endpoints - setupAPI(d) - - // Process some tasks - processTasks(d) - - // Display basic statistics - displayStatistics(d) - - // Start HTTP server for API - fmt.Println("Starting HTTP server on :8080") - fmt.Println("Visit http://localhost:8080 for the dashboard") - log.Fatal(http.ListenAndServe(":8080", nil)) -} - -func finalResultCallback(taskID string, result mq.Result) { - fmt.Printf("Task %s completed with status: %v\n", taskID, result.Status) -} - -func buildDAG(d *dag.DAG) { - // Add nodes in a linear flow to avoid cycles - using proper processor types - d.AddNode(dag.Function, "Start Node", "start", &StartProcessor{}, true) - d.AddNode(dag.Function, "Process Node", "process", &ProcessorNode{}) - d.AddNode(dag.Function, "Validate Node", "validate", &ValidatorNode{}) - d.AddNode(dag.Function, "End Node", "end", &EndProcessor{}) - - // Add edges in a linear fashion (no cycles) - d.AddEdge(dag.Simple, "start-to-process", "start", "process") - d.AddEdge(dag.Simple, "process-to-validate", "process", "validate") - d.AddEdge(dag.Simple, "validate-to-end", "validate", "end") - - fmt.Println("DAG structure built successfully") -} - -func setupAPI(d *dag.DAG) { - // Basic status endpoint - http.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - status := map[string]interface{}{ - "status": "running", - "dag_name": d.GetType(), - "timestamp": time.Now(), - } - json.NewEncoder(w).Encode(status) - }) - - // Task metrics endpoint - http.HandleFunc("/api/metrics", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - metrics := d.GetTaskMetrics() - // Create a safe copy to avoid lock issues - safeMetrics := map[string]interface{}{ - "completed": metrics.Completed, - "failed": metrics.Failed, - "cancelled": metrics.Cancelled, - "not_started": metrics.NotStarted, - "queued": metrics.Queued, - } - json.NewEncoder(w).Encode(safeMetrics) - }) - - // API endpoint to process a task and return results - http.HandleFunc("/api/process", func(w http.ResponseWriter, r *http.Request) { - taskData := map[string]interface{}{ - "id": fmt.Sprintf("api-task-%d", time.Now().UnixNano()), - "payload": "api-data", - "timestamp": time.Now(), - } - payload, _ := json.Marshal(taskData) - - fmt.Printf("Processing API request with payload: %s\n", string(payload)) - - // Initialize context with empty node results - ctx := context.WithValue(context.Background(), "nodeResults", make(map[string]interface{})) - result := d.Process(ctx, payload) - - fmt.Printf("Processing completed. Status: %v\n", result.Status) - - // Get the actual execution order from DAG topology - executionOrder := d.TopologicalSort() - fmt.Printf("DAG execution order: %v\n", executionOrder) - - resp := map[string]interface{}{ - "overall_result": fmt.Sprintf("%v", result.Status), - "task_id": result.TaskID, - "payload": result.Payload, - "timestamp": time.Now(), - "execution_order": executionOrder, // Include the actual execution order - } - - if result.Error != nil { - resp["error"] = result.Error.Error() - fmt.Printf("Error occurred: %v\n", result.Error) - } - - // Extract node results from context - if nodeResults, ok := result.Ctx.Value("nodeResults").(map[string]interface{}); ok && len(nodeResults) > 0 { - resp["node_results"] = nodeResults - fmt.Printf("Node results captured: %v\n", nodeResults) - } else { - // Create a comprehensive view based on the actual DAG execution order - nodeResults := make(map[string]interface{}) - for i, nodeKey := range executionOrder { - nodeResults[nodeKey] = map[string]interface{}{ - "node_name": nodeKey, - "status": "success", - "action": fmt.Sprintf("executed_step_%d", i+1), - "executed_at": time.Now().Format("15:04:05"), - "execution_step": i + 1, - } - } - resp["node_results"] = nodeResults - fmt.Printf("📝 Created node results based on DAG topology: %v\n", nodeResults) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - }) - - // DAG Diagram endpoint - generates and serves PNG diagram - http.HandleFunc("/api/diagram", func(w http.ResponseWriter, r *http.Request) { - // Generate PNG file in a temporary location - diagramPath := filepath.Join(os.TempDir(), fmt.Sprintf("dag-%d.png", time.Now().UnixNano())) - - fmt.Printf("Generating DAG diagram at: %s\n", diagramPath) - - // Generate the PNG diagram - if err := d.SavePNG(diagramPath); err != nil { - fmt.Printf("Failed to generate diagram: %v\n", err) - http.Error(w, fmt.Sprintf("Failed to generate diagram: %v", err), http.StatusInternalServerError) - return - } - - // Ensure cleanup - defer func() { - if err := os.Remove(diagramPath); err != nil { - fmt.Printf("Failed to cleanup diagram file: %v\n", err) - } - }() - - // Serve the PNG file - w.Header().Set("Content-Type", "image/png") - w.Header().Set("Cache-Control", "no-cache") - http.ServeFile(w, r, diagramPath) - - fmt.Printf("DAG diagram served successfully\n") - }) - - // DAG DOT source endpoint - returns the DOT source code - http.HandleFunc("/api/dot", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - dotContent := d.ExportDOT() - w.Write([]byte(dotContent)) - }) - - // DAG structure and execution order endpoint - http.HandleFunc("/api/structure", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - executionOrder := d.TopologicalSort() - - structure := map[string]interface{}{ - "execution_order": executionOrder, - "dag_name": d.GetType(), - "dag_key": d.GetKey(), - "total_nodes": len(executionOrder), - "timestamp": time.Now(), - } - - json.NewEncoder(w).Encode(structure) - }) - - // Root dashboard - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, ` - - - - Enhanced DAG Demo - - - -
-
-

Enhanced DAG Demo Dashboard

-

DAG is running successfully!

-
- -
- - - -
- -
-
-

API Endpoints

-
- GET - /api/status - Get DAG status -
-
- GET - /api/metrics - Get task metrics -
-
- POST - /api/process - Process a new task -
-
- GET - /api/diagram - Get DAG diagram (PNG) -
-
- GET - /api/dot - Get DAG structure (DOT format) -
-
- GET - /api/structure - Get DAG structure and execution order -
-
-
- -
-
-

DAG Visual Structure

-

Flow: Start -> Process -> Validate -> End

-

Type: Linear (Cycle-free)

-

This structure ensures no circular dependencies while demonstrating the enhanced features.

- -
- -
- - -
-
- -
-
-

Run Example Task

- -
-
-
-
- - - - `) - }) -} - -func processTasks(d *dag.DAG) { - fmt.Println("Processing example tasks...") - - for i := 0; i < 3; i++ { - taskData := map[string]interface{}{ - "id": fmt.Sprintf("task-%d", i), - "payload": fmt.Sprintf("example-data-%d", i), - "timestamp": time.Now(), - } - - payload, _ := json.Marshal(taskData) - - fmt.Printf("Processing task %d...\n", i) - result := d.Process(context.Background(), payload) - - // Show overall result - if result.Error == nil { - fmt.Printf("Task %d completed successfully\n", i) - } else { - fmt.Printf("Task %d failed: %v\n", i, result.Error) - } - - // Show per-node results in DAG execution order - if nodeResults, ok := result.Ctx.Value("nodeResults").(map[string]interface{}); ok { - fmt.Println("Node Results (DAG Execution Order):") - // Get the actual execution order from DAG topology - executionOrder := d.TopologicalSort() - nodeLabels := map[string]string{ - "start": "Start Node", - "process": "Process Node", - "validate": "Validate Node", - "end": "End Node", - } - - stepNum := 1 - for _, nodeKey := range executionOrder { - if res, exists := nodeResults[nodeKey]; exists { - label := nodeLabels[nodeKey] - if label == "" { - // Capitalize first letter of node key - if len(nodeKey) > 0 { - label = fmt.Sprintf("%s%s Node", strings.ToUpper(nodeKey[:1]), nodeKey[1:]) - } else { - label = fmt.Sprintf("%s Node", nodeKey) - } - } - fmt.Printf(" Step %d - %s: %v\n", stepNum, label, res) - stepNum++ - } - } - - // Show the execution order - fmt.Printf(" DAG Execution Order: %s\n", strings.Join(executionOrder, " → ")) - } - - time.Sleep(200 * time.Millisecond) - } - - fmt.Println("Task processing completed!") -} - -func displayStatistics(d *dag.DAG) { - fmt.Println("\n=== DAG Statistics ===") - - // Get basic task metrics - metrics := d.GetTaskMetrics() - fmt.Printf("Task Metrics:\n") - fmt.Printf(" Completed: %d\n", metrics.Completed) - fmt.Printf(" Failed: %d\n", metrics.Failed) - fmt.Printf(" Cancelled: %d\n", metrics.Cancelled) - fmt.Printf(" Not Started: %d\n", metrics.NotStarted) - fmt.Printf(" Queued: %d\n", metrics.Queued) - - // Get DAG information - fmt.Printf("\nDAG Information:\n") - fmt.Printf(" Name: %s\n", d.GetType()) - fmt.Printf(" Key: %s\n", d.GetKey()) - - // Check if DAG is ready - if d.IsReady() { - fmt.Printf(" Status: Ready\n") - } else { - fmt.Printf(" Status: Not Ready\n") - } - - fmt.Println("\n=== End Statistics ===\n") -} diff --git a/examples/config/production.json b/examples/config/production.json deleted file mode 100644 index 72a41bc..0000000 --- a/examples/config/production.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "broker": { - "address": "localhost", - "port": 8080, - "max_connections": 1000, - "connection_timeout": "0s", - "read_timeout": "0s", - "write_timeout": "0s", - "idle_timeout": "0s", - "keep_alive": true, - "keep_alive_period": "60s", - "max_queue_depth": 10000, - "enable_dead_letter": true, - "dead_letter_max_retries": 3 - }, - "consumer": { - "enable_http_api": true, - "max_retries": 5, - "initial_delay": "2s", - "max_backoff": "30s", - "jitter_percent": 0.5, - "batch_size": 10, - "prefetch_count": 100, - "auto_ack": false, - "requeue_on_failure": true - }, - "publisher": { - "enable_http_api": true, - "max_retries": 3, - "initial_delay": "1s", - "max_backoff": "10s", - "jitter_percent": 0.5, - "connection_pool_size": 10, - "publish_timeout": "5s", - "enable_batching": false, - "batch_size": 100, - "batch_timeout": "1s" - }, - "pool": { - "min_workers": 1, - "max_workers": 100, - "queue_size": 1000, - "max_memory_load": 1073741824, - "task_timeout": "30s", - "idle_worker_timeout": "5m", - "enable_dynamic_scaling": true, - "scaling_factor": 1.5, - "scaling_interval": "1m", - "max_queue_wait_time": "10s", - "enable_work_stealing": false, - "enable_priority_scheduling": true, - "graceful_shutdown_timeout": "30s" - }, - "security": { - "enable_tls": false, - "tls_cert_path": "", - "tls_key_path": "", - "tls_ca_path": "", - "tls_insecure_skip_verify": false, - "enable_authentication": false, - "authentication_method": "basic", - "enable_authorization": false, - "enable_encryption": false, - "encryption_key": "", - "enable_audit_log": false, - "audit_log_path": "/var/log/mq/audit.log", - "session_timeout": "30m", - "max_login_attempts": 3, - "lockout_duration": "15m" - }, - "monitoring": { - "enable_metrics": true, - "metrics_port": 9090, - "metrics_path": "/metrics", - "enable_health_check": true, - "health_check_port": 8081, - "health_check_path": "/health", - "health_check_interval": "30s", - "enable_tracing": false, - "tracing_endpoint": "", - "tracing_sample_rate": 0.1, - "enable_logging": true, - "log_level": "info", - "log_format": "json", - "log_output": "stdout", - "log_file_path": "/var/log/mq/app.log", - "log_max_size": 100, - "log_max_backups": 10, - "log_max_age": 30, - "enable_profiling": false, - "profiling_port": 6060 - }, - "persistence": { - "enable_persistence": false, - "storage_type": "memory", - "connection_string": "", - "max_connections": 10, - "connection_timeout": "10s", - "retention_period": "168h", - "cleanup_interval": "1h", - "backup_enabled": false, - "backup_interval": "6h", - "backup_path": "/var/backup/mq", - "compression_enabled": true, - "encryption_enabled": false, - "replication_enabled": false, - "replication_nodes": [ ] - }, - "clustering": { - "enable_clustering": false, - "node_id": "", - "cluster_nodes": [ ], - "discovery_method": "static", - "discovery_endpoint": "", - "heartbeat_interval": "5s", - "election_timeout": "15s", - "enable_load_balancing": false, - "load_balancing_strategy": "round_robin", - "enable_failover": false, - "failover_timeout": "30s", - "enable_replication": false, - "replication_factor": 3, - "consistency_level": "strong" - }, - "rate_limit": { - "enable_broker_rate_limit": false, - "broker_rate": 1000, - "broker_burst": 100, - "enable_consumer_rate_limit": false, - "consumer_rate": 100, - "consumer_burst": 10, - "enable_publisher_rate_limit": false, - "publisher_rate": 100, - "publisher_burst": 10, - "enable_per_queue_rate_limit": false, - "per_queue_rate": 50, - "per_queue_burst": 5 - } -} diff --git a/examples/consumer.go b/examples/consumer.go deleted file mode 100644 index 3ca7842..0000000 --- a/examples/consumer.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "context" - - "github.com/oarkflow/mq" - - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - n := &tasks.Node6{} - consumer1 := mq.NewConsumer("F", "queue1", n.ProcessTask, mq.WithBrokerURL(":8081"), mq.WithHTTPApi(true), mq.WithWorkerPool(100, 4, 50000)) - consumer1.Consume(context.Background()) -} diff --git a/examples/dag.go b/examples/dag.go deleted file mode 100644 index cee5e03..0000000 --- a/examples/dag.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/oarkflow/json" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/examples/tasks" -) - -func subDAG() *dag.DAG { - f := dag.NewDAG("Sub DAG", "sub-dag", func(taskID string, result mq.Result) { - fmt.Printf("Sub DAG Final result for task %s: %s\n", taskID, string(result.Payload)) - }, mq.WithSyncMode(true)) - f. - AddNode(dag.Function, "Store data", "store:data", &tasks.StoreData{Operation: dag.Operation{Type: dag.Function}}, true). - AddNode(dag.Function, "Send SMS", "send:sms", &tasks.SendSms{Operation: dag.Operation{Type: dag.Function}}). - AddNode(dag.Function, "Notification", "notification", &tasks.InAppNotification{Operation: dag.Operation{Type: dag.Function}}). - AddEdge(dag.Simple, "Store Payload to send sms", "store:data", "send:sms"). - AddEdge(dag.Simple, "Store Payload to notification", "send:sms", "notification") - return f -} - -func main() { - flow := dag.NewDAG("Sample DAG", "sample-dag", func(taskID string, result mq.Result) { - fmt.Printf("DAG Final result for task %s: %s\n", taskID, string(result.Payload)) - }) - flow.AddNode(dag.Function, "GetData", "GetData", &GetData{}, true) - flow.AddNode(dag.Function, "Loop", "Loop", &Loop{}) - flow.AddNode(dag.Function, "ValidateAge", "ValidateAge", &ValidateAge{}) - flow.AddNode(dag.Function, "ValidateGender", "ValidateGender", &ValidateGender{}) - flow.AddNode(dag.Function, "Final", "Final", &Final{}) - flow.AddDAGNode(dag.Function, "Check", "persistent", subDAG()) - flow.AddEdge(dag.Simple, "GetData", "GetData", "Loop") - flow.AddEdge(dag.Iterator, "Validate age for each item", "Loop", "ValidateAge") - flow.AddCondition("ValidateAge", map[string]string{"pass": "ValidateGender", "default": "persistent"}) - flow.AddEdge(dag.Simple, "Mark as Done", "Loop", "Final") - - // flow.Start(":8080") - data := []byte(`[{"age": "15", "gender": "female"}, {"age": "18", "gender": "male"}]`) - if flow.Error != nil { - panic(flow.Error) - } - - rs := flow.Process(context.Background(), data) - if rs.Error != nil { - panic(rs.Error) - } - fmt.Println(rs.Status, rs.Topic, string(rs.Payload)) -} - -type GetData struct { - dag.Operation -} - -func (p *GetData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return mq.Result{Ctx: ctx, Payload: task.Payload} -} - -type Loop struct { - dag.Operation -} - -func (p *Loop) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return mq.Result{Ctx: ctx, Payload: task.Payload} -} - -type ValidateAge struct { - dag.Operation -} - -func (p *ValidateAge) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: fmt.Errorf("ValidateAge Error: %s", err.Error()), Ctx: ctx} - } - var status string - if data["age"] == "18" { - status = "pass" - } else { - status = "default" - } - updatedPayload, _ := json.Marshal(data) - return mq.Result{Payload: updatedPayload, Ctx: ctx, ConditionStatus: status} -} - -type ValidateGender struct { - dag.Operation -} - -func (p *ValidateGender) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: fmt.Errorf("ValidateGender Error: %s", err.Error()), Ctx: ctx} - } - data["female_voter"] = data["gender"] == "female" - updatedPayload, _ := json.Marshal(data) - return mq.Result{Payload: updatedPayload, Ctx: ctx} -} - -type Final struct { - dag.Operation -} - -func (p *Final) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data []map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: fmt.Errorf("Final Error: %s", err.Error()), Ctx: ctx} - } - for i, row := range data { - row["done"] = true - data[i] = row - } - updatedPayload, err := json.Marshal(data) - if err != nil { - panic(err) - } - return mq.Result{Payload: updatedPayload, Ctx: ctx} -} diff --git a/examples/dag_consumer.go b/examples/dag_consumer.go deleted file mode 100644 index 921e5e2..0000000 --- a/examples/dag_consumer.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "context" - "fmt" - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - d := dag.NewDAG("Sample DAG", "sample-dag", func(taskID string, result mq.Result) { - fmt.Println("Final", string(result.Payload)) - }, - mq.WithSyncMode(true), - mq.WithNotifyResponse(tasks.NotifyResponse), - ) - d.AddNode(dag.Function, "C", "C", &tasks.Node3{}, true) - d.AddNode(dag.Function, "D", "D", &tasks.Node4{}) - d.AddNode(dag.Function, "E", "E", &tasks.Node5{}) - d.AddNode(dag.Function, "F", "F", &tasks.Node6{}) - d.AddNode(dag.Function, "G", "G", &tasks.Node7{}) - d.AddNode(dag.Function, "H", "H", &tasks.Node8{}) - - d.AddCondition("C", map[string]string{"PASS": "D", "FAIL": "E"}) - d.AddEdge(dag.Simple, "Label 1", "B", "C") - d.AddEdge(dag.Simple, "Label 2", "D", "F") - d.AddEdge(dag.Simple, "Label 3", "E", "F") - d.AddEdge(dag.Simple, "Label 4", "F", "G", "H") - d.AssignTopic("queue") - err := d.Consume(context.Background()) - if err != nil { - panic(err) - } -} diff --git a/examples/data_transform_demo.go b/examples/data_transform_demo.go deleted file mode 100644 index e545cf2..0000000 --- a/examples/data_transform_demo.go +++ /dev/null @@ -1,827 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/handlers" -) - -func main() { - fmt.Println("=== Data Transformation Handlers Examples ===") - - // Test each handler with sample data - testFormatHandler() - testGroupHandler() - testSplitJoinHandler() - testFlattenHandler() - testJSONHandler() - testFieldHandler() - testDataHandler() - - // Example of chaining handlers - exampleDAGChaining() -} - -func testFormatHandler() { - fmt.Println("\n1. FORMAT HANDLER TESTS") - fmt.Println("========================") - - // Test uppercase formatting - testData := map[string]any{ - "name": "john doe", - "title": "software engineer", - "age": 30, - } - - handler := handlers.NewFormatHandler("format-test") - config := dag.Payload{ - Data: map[string]any{ - "format_type": "uppercase", - "fields": []string{"name", "title"}, - }, - } - handler.SetConfig(config) - - result := runHandler(handler, testData, "Uppercase Format") - printResult("Uppercase formatting", result) - printRequestConfigResult(testData, config, result) - - // Test currency formatting - currencyData := map[string]any{ - "price": 99.99, - "tax": "15.50", - "total": 115.49, - } - - currencyHandler := handlers.NewFormatHandler("currency-test") - currencyConfig := dag.Payload{ - Data: map[string]any{ - "format_type": "currency", - "fields": []string{"price", "tax", "total"}, - "currency": "$", - }, - } - currencyHandler.SetConfig(currencyConfig) - - result = runHandler(currencyHandler, currencyData, "Currency Format") - printResult("Currency formatting", result) - printRequestConfigResult(currencyData, currencyConfig, result) - - // Test date formatting - dateData := map[string]any{ - "created_at": "2023-06-15T10:30:00Z", - "updated_at": "2023-06-20", - } - - dateHandler := handlers.NewFormatHandler("date-test") - dateConfig := dag.Payload{ - Data: map[string]any{ - "format_type": "date", - "fields": []string{"created_at", "updated_at"}, - "date_format": "2006-01-02", - }, - } - dateHandler.SetConfig(dateConfig) - - result = runHandler(dateHandler, dateData, "Date Format") - printResult("Date formatting", result) - printRequestConfigResult(dateData, dateConfig, result) -} - -func testGroupHandler() { - fmt.Println("\n2. GROUP HANDLER TESTS") - fmt.Println("======================") - - // Test data grouping with aggregation - testData := map[string]any{ - "data": []interface{}{ - map[string]any{"department": "Engineering", "salary": 80000, "age": 30, "name": "John"}, - map[string]any{"department": "Engineering", "salary": 90000, "age": 25, "name": "Jane"}, - map[string]any{"department": "Marketing", "salary": 60000, "age": 35, "name": "Bob"}, - map[string]any{"department": "Marketing", "salary": 65000, "age": 28, "name": "Alice"}, - map[string]any{"department": "Engineering", "salary": 95000, "age": 32, "name": "Mike"}, - }, - } - - handler := handlers.NewGroupHandler("group-test") - config := dag.Payload{ - Data: map[string]any{ - "group_by": []string{"department"}, - "aggregations": map[string]any{ - "salary": "sum", - "age": "avg", - "name": "concat", - }, - "concat_separator": ", ", - }, - } - handler.SetConfig(config) - - result := runHandler(handler, testData, "Group by Department") - printResult("Data grouping", result) - printRequestConfigResult(testData, config, result) -} - -func testSplitJoinHandler() { - fmt.Println("\n3. SPLIT/JOIN HANDLER TESTS") - fmt.Println("============================") - - // Test split operation - testData := map[string]any{ - "full_name": "John Michael Doe", - "tags": "go,programming,backend,api", - "skills": "golang python javascript", - } - - splitHandler := handlers.NewSplitHandler("split-test") - splitConfig := dag.Payload{ - Data: map[string]any{ - "operation": "split", - "fields": []string{"full_name", "skills"}, - "separator": " ", - }, - } - splitHandler.SetConfig(splitConfig) - - result := runHandler(splitHandler, testData, "Split Operation (space)") - printResult("String splitting with space", result) - printRequestConfigResult(testData, splitConfig, result) - - // Test split with comma - splitHandler2 := handlers.NewSplitHandler("split-test-2") - splitConfig2 := dag.Payload{ - Data: map[string]any{ - "operation": "split", - "fields": []string{"tags"}, - "separator": ",", - }, - } - splitHandler2.SetConfig(splitConfig2) - - result = runHandler(splitHandler2, testData, "Split Operation (comma)") - printResult("String splitting with comma", result) - printRequestConfigResult(testData, splitConfig2, result) - - // Test join operation - joinData := map[string]any{ - "first_name": "John", - "middle_name": "Michael", - "last_name": "Doe", - "title": "Mr.", - } - - joinHandler := handlers.NewJoinHandler("join-test") - joinConfig := dag.Payload{ - Data: map[string]any{ - "operation": "join", - "source_fields": []string{"title", "first_name", "middle_name", "last_name"}, - "target_field": "full_name_with_title", - "separator": " ", - }, - } - joinHandler.SetConfig(joinConfig) - - result = runHandler(joinHandler, joinData, "Join Operation") - printResult("String joining", result) - printRequestConfigResult(joinData, joinConfig, result) - - fmt.Printf("Split Test Data: %+v\n", testData) - fmt.Printf("Split Config: %+v\n", splitConfig.Data) - fmt.Printf("Split Result: %+v\n", result) - - fmt.Printf("Split Test Data (comma): %+v\n", testData) - fmt.Printf("Split Config (comma): %+v\n", splitConfig2.Data) - fmt.Printf("Split Result (comma): %+v\n", result) - - fmt.Printf("Join Test Data: %+v\n", joinData) - fmt.Printf("Join Config: %+v\n", joinConfig.Data) - fmt.Printf("Join Result: %+v\n", result) -} - -func testFlattenHandler() { - fmt.Println("\n4. FLATTEN HANDLER TESTS") - fmt.Println("=========================") - - // Test flatten settings - testData := map[string]any{ - "user_id": 123, - "settings": []interface{}{ - map[string]any{"key": "theme", "value": "dark", "value_type": "string"}, - map[string]any{"key": "notifications", "value": "true", "value_type": "boolean"}, - map[string]any{"key": "max_items", "value": "50", "value_type": "integer"}, - map[string]any{"key": "timeout", "value": "30.5", "value_type": "float"}, - }, - } - - handler := handlers.NewFlattenHandler("flatten-test") - config := dag.Payload{ - Data: map[string]any{ - "operation": "flatten_settings", - "source_field": "settings", - "target_field": "user_config", - }, - } - handler.SetConfig(config) - - result := runHandler(handler, testData, "Flatten Settings") - printResult("Settings flattening", result) - printRequestConfigResult(testData, config, result) - - // Test flatten key-value pairs - kvData := map[string]any{ - "user_id": 456, - "properties": []interface{}{ - map[string]any{"name": "color", "val": "blue"}, - map[string]any{"name": "size", "val": "large"}, - map[string]any{"name": "weight", "val": "heavy"}, - }, - } - - kvHandler := handlers.NewFlattenHandler("kv-test") - kvConfig := dag.Payload{ - Data: map[string]any{ - "operation": "flatten_key_value", - "source_field": "properties", - "key_field": "name", - "value_field": "val", - "target_field": "flattened_props", - }, - } - kvHandler.SetConfig(kvConfig) - - result = runHandler(kvHandler, kvData, "Flatten Key-Value") - printResult("Key-value flattening", result) - printRequestConfigResult(kvData, kvConfig, result) - - // Test flatten nested objects - nestedData := map[string]any{ - "user": map[string]any{ - "id": 123, - "profile": map[string]any{ - "name": "John Doe", - "email": "john@example.com", - "address": map[string]any{ - "street": "123 Main St", - "city": "New York", - "country": "USA", - }, - "preferences": map[string]any{ - "theme": "dark", - "language": "en", - }, - }, - }, - } - - nestedHandler := handlers.NewFlattenHandler("nested-test") - nestedConfig := dag.Payload{ - Data: map[string]any{ - "operation": "flatten_nested_objects", - "separator": "_", - }, - } - nestedHandler.SetConfig(nestedConfig) - - result = runHandler(nestedHandler, nestedData, "Flatten Nested Objects") - printResult("Nested object flattening", result) - printRequestConfigResult(nestedData, nestedConfig, result) -} - -func testJSONHandler() { - fmt.Println("\n5. JSON HANDLER TESTS") - fmt.Println("=====================") - - // Test JSON parsing - testData := map[string]any{ - "config": `{"theme": "dark", "language": "en", "notifications": true, "max_items": 100}`, - "metadata": `["tag1", "tag2", "tag3"]`, - "user": `{"id": 123, "name": "John Doe", "active": true}`, - } - - parseHandler := handlers.NewJSONHandler("json-parse-test") - parseConfig := dag.Payload{ - Data: map[string]any{ - "operation": "parse", - "fields": []string{"config", "metadata", "user"}, - }, - } - parseHandler.SetConfig(parseConfig) - - result := runHandler(parseHandler, testData, "JSON Parsing") - printResult("JSON parsing", result) - printRequestConfigResult(testData, parseConfig, result) - - // Test JSON stringifying - objData := map[string]any{ - "user": map[string]any{ - "id": 123, - "name": "John Doe", - "active": true, - "roles": []string{"admin", "user"}, - }, - "preferences": map[string]any{ - "theme": "dark", - "notifications": true, - "language": "en", - }, - } - - stringifyHandler := handlers.NewJSONHandler("json-stringify-test") - stringifyConfig := dag.Payload{ - Data: map[string]any{ - "operation": "stringify", - "fields": []string{"user", "preferences"}, - "indent": true, - }, - } - stringifyHandler.SetConfig(stringifyConfig) - - result = runHandler(stringifyHandler, objData, "JSON Stringifying") - printResult("JSON stringifying", result) - printRequestConfigResult(objData, stringifyConfig, result) - - // Test JSON validation - validationData := map[string]any{ - "valid_json": `{"key": "value"}`, - "invalid_json": `{"key": value}`, // Missing quotes around value - "valid_array": `[1, 2, 3]`, - } - - validateHandler := handlers.NewJSONHandler("json-validate-test") - validateConfig := dag.Payload{ - Data: map[string]any{ - "operation": "validate", - "fields": []string{"valid_json", "invalid_json", "valid_array"}, - }, - } - validateHandler.SetConfig(validateConfig) - - result = runHandler(validateHandler, validationData, "JSON Validation") - printResult("JSON validation", result) - printRequestConfigResult(validationData, validateConfig, result) -} - -func testFieldHandler() { - fmt.Println("\n6. FIELD HANDLER TESTS") - fmt.Println("======================") - - testData := map[string]any{ - "id": 123, - "first_name": "John", - "last_name": "Doe", - "email_addr": "john@example.com", - "phone_number": "555-1234", - "internal_id": "INT-123", - "created_at": "2023-01-15", - "updated_at": "2023-06-20", - "is_active": true, - "salary": 75000.50, - } - - // Test field filtering/selection - filterHandler := handlers.NewFieldHandler("filter-test") - filterConfig := dag.Payload{ - Data: map[string]any{ - "operation": "filter", - "fields": []string{"id", "first_name", "last_name", "email_addr", "is_active"}, - }, - } - filterHandler.SetConfig(filterConfig) - - result := runHandler(filterHandler, testData, "Filter/Select Fields") - printResult("Field filtering", result) - printRequestConfigResult(testData, filterConfig, result) - - // Test field exclusion/removal - excludeHandler := handlers.NewFieldHandler("exclude-test") - excludeConfig := dag.Payload{ - Data: map[string]any{ - "operation": "exclude", - "fields": []string{"internal_id", "created_at", "updated_at"}, - }, - } - excludeHandler.SetConfig(excludeConfig) - - result = runHandler(excludeHandler, testData, "Exclude Fields") - printResult("Field exclusion", result) - printRequestConfigResult(testData, excludeConfig, result) - - // Test field renaming - renameHandler := handlers.NewFieldHandler("rename-test") - renameConfig := dag.Payload{ - Data: map[string]any{ - "operation": "rename", - "mapping": map[string]any{ - "first_name": "firstName", - "last_name": "lastName", - "email_addr": "email", - "phone_number": "phone", - "created_at": "createdAt", - "updated_at": "updatedAt", - "is_active": "active", - }, - }, - } - renameHandler.SetConfig(renameConfig) - - result = runHandler(renameHandler, testData, "Rename Fields") - printResult("Field renaming", result) - printRequestConfigResult(testData, renameConfig, result) - - // Test adding new fields - addHandler := handlers.NewFieldHandler("add-test") - addConfig := dag.Payload{ - Data: map[string]any{ - "operation": "add", - "new_fields": map[string]any{ - "status": "active", - "version": "1.0", - "is_verified": true, - "last_login": "2023-06-20T10:30:00Z", - "department": "Engineering", - "access_level": 3, - }, - }, - } - addHandler.SetConfig(addConfig) - - result = runHandler(addHandler, testData, "Add Fields") - printResult("Adding fields", result) - printRequestConfigResult(testData, addConfig, result) - - // Test field copying - copyHandler := handlers.NewFieldHandler("copy-test") - copyConfig := dag.Payload{ - Data: map[string]any{ - "operation": "copy", - "mapping": map[string]any{ - "first_name": "display_name", - "email_addr": "contact_email", - "id": "user_id", - }, - }, - } - copyHandler.SetConfig(copyConfig) - - result = runHandler(copyHandler, testData, "Copy Fields") - printResult("Field copying", result) - printRequestConfigResult(testData, copyConfig, result) - - // Test key transformation - transformHandler := handlers.NewFieldHandler("transform-test") - transformConfig := dag.Payload{ - Data: map[string]any{ - "operation": "transform_keys", - "transformation": "snake_case", - }, - } - transformHandler.SetConfig(transformConfig) - - result = runHandler(transformHandler, testData, "Transform Keys") - printResult("Key transformation", result) - printRequestConfigResult(testData, transformConfig, result) -} - -func testDataHandler() { - fmt.Println("\n7. DATA HANDLER TESTS") - fmt.Println("=====================") - - // Test data sorting - testData := map[string]any{ - "data": []interface{}{ - map[string]any{"name": "John", "age": 30, "salary": 80000, "department": "Engineering"}, - map[string]any{"name": "Jane", "age": 25, "salary": 90000, "department": "Engineering"}, - map[string]any{"name": "Bob", "age": 35, "salary": 75000, "department": "Marketing"}, - map[string]any{"name": "Alice", "age": 28, "salary": 85000, "department": "Marketing"}, - }, - } - - sortHandler := handlers.NewDataHandler("sort-test") - sortConfig := dag.Payload{ - Data: map[string]any{ - "operation": "sort", - "sort_field": "salary", - "sort_order": "desc", - }, - } - sortHandler.SetConfig(sortConfig) - - result := runHandler(sortHandler, testData, "Sort Data by Salary (Desc)") - printResult("Data sorting", result) - printRequestConfigResult(testData, sortConfig, result) - - // Test field calculations - calcData := map[string]any{ - "base_price": 100.0, - "tax_rate": 0.15, - "shipping_cost": 10.0, - "discount": 5.0, - "quantity": 2, - } - - calcHandler := handlers.NewDataHandler("calc-test") - calcConfig := dag.Payload{ - Data: map[string]any{ - "operation": "calculate", - "calculations": map[string]any{ - "tax_amount": map[string]any{ - "operation": "multiply", - "fields": []string{"base_price", "tax_rate"}, - }, - "subtotal": map[string]any{ - "operation": "sum", - "fields": []string{"base_price", "tax_amount", "shipping_cost"}, - }, - "total": map[string]any{ - "operation": "subtract", - "fields": []string{"subtotal", "discount"}, - }, - "grand_total": map[string]any{ - "operation": "multiply", - "fields": []string{"total", "quantity"}, - }, - }, - }, - } - calcHandler.SetConfig(calcConfig) - - result = runHandler(calcHandler, calcData, "Field Calculations") - printResult("Field calculations", result) - printRequestConfigResult(calcData, calcConfig, result) - - // Test data deduplication - dupData := map[string]any{ - "data": []interface{}{ - map[string]any{"email": "john@example.com", "name": "John Doe", "id": 1}, - map[string]any{"email": "jane@example.com", "name": "Jane Smith", "id": 2}, - map[string]any{"email": "john@example.com", "name": "John D.", "id": 3}, // duplicate email - map[string]any{"email": "bob@example.com", "name": "Bob Jones", "id": 4}, - map[string]any{"email": "jane@example.com", "name": "Jane S.", "id": 5}, // duplicate email - }, - } - - dedupHandler := handlers.NewDataHandler("dedup-test") - dedupConfig := dag.Payload{ - Data: map[string]any{ - "operation": "deduplicate", - "dedupe_fields": []string{"email"}, - }, - } - dedupHandler.SetConfig(dedupConfig) - - result = runHandler(dedupHandler, dupData, "Data Deduplication") - printResult("Data deduplication", result) - printRequestConfigResult(dupData, dedupConfig, result) - - // Test type casting - castData := map[string]any{ - "user_id": "123", - "age": "30", - "salary": "75000.50", - "is_active": "true", - "score": "95.5", - "name": 123, - "is_verified": "false", - } - - castHandler := handlers.NewDataHandler("cast-test") - castConfig := dag.Payload{ - Data: map[string]any{ - "operation": "type_cast", - "cast": map[string]any{ - "user_id": "int", - "age": "int", - "salary": "float", - "is_active": "bool", - "score": "float", - "name": "string", - "is_verified": "bool", - }, - }, - } - castHandler.SetConfig(castConfig) - - result = runHandler(castHandler, castData, "Type Casting") - printResult("Type casting", result) - printRequestConfigResult(castData, castConfig, result) - - // Test conditional field setting - condData := map[string]any{ - "age": 25, - "salary": 60000, - "years_experience": 3, - } - - condHandler := handlers.NewDataHandler("conditional-test") - condConfig := dag.Payload{ - Data: map[string]any{ - "operation": "conditional_set", - "conditions": map[string]any{ - "salary_level": map[string]any{ - "condition": "salary > 70000", - "if_true": "high", - "if_false": "standard", - }, - "experience_level": map[string]any{ - "condition": "years_experience >= 5", - "if_true": "senior", - "if_false": "junior", - }, - }, - }, - } - condHandler.SetConfig(condConfig) - - result = runHandler(condHandler, condData, "Conditional Field Setting") - printResult("Conditional setting", result) - printRequestConfigResult(condData, condConfig, result) -} - -// Helper functions -func runHandler(handler dag.Processor, data map[string]any, description string) map[string]any { - fmt.Printf("\n--- Testing: %s ---\n", description) - - // Convert data to JSON payload - payload, err := json.Marshal(data) - if err != nil { - log.Printf("Error marshaling test data: %v", err) - return nil - } - - // Create a task - task := &mq.Task{ - ID: mq.NewID(), - Payload: payload, - } - - // Process the task - ctx := context.Background() - result := handler.ProcessTask(ctx, task) - - if result.Error != nil { - log.Printf("Handler error: %v", result.Error) - return nil - } - - // Parse result payload - var resultData map[string]any - if err := json.Unmarshal(result.Payload, &resultData); err != nil { - log.Printf("Error unmarshaling result: %v", err) - return nil - } - - return resultData -} - -func printResult(operation string, result map[string]any) { - if result == nil { - fmt.Printf("❌ %s failed\n", operation) - return - } - - fmt.Printf("✅ %s succeeded\n", operation) - - // Pretty print the result (truncated for readability) - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - fmt.Printf("Error formatting result: %v\n", err) - return - } - - // Truncate very long results - resultStr := string(resultJSON) - if len(resultStr) > 1000 { - resultStr = resultStr[:997] + "..." - } -} - -func printRequestConfigResult(requestData map[string]any, config dag.Payload, result map[string]any) { - fmt.Println("\n=== Request Data ===") - requestJSON, err := json.MarshalIndent(requestData, "", " ") - if err != nil { - fmt.Printf("Error formatting request data: %v\n", err) - } else { - fmt.Println(string(requestJSON)) - } - - fmt.Println("\n=== Configuration ===") - configJSON, err := json.MarshalIndent(config.Data, "", " ") - if err != nil { - fmt.Printf("Error formatting configuration: %v\n", err) - } else { - fmt.Println(string(configJSON)) - } - - fmt.Println("\n=== Result ===") - if result == nil { - fmt.Println("❌ Operation failed") - } else { - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - fmt.Printf("Error formatting result: %v\n", err) - } else { - fmt.Println(string(resultJSON)) - } - } -} - -// Example of chaining handlers in a DAG workflow -func exampleDAGChaining() { - fmt.Println("\n=== CHAINING HANDLERS EXAMPLE ===") - fmt.Println("==================================") - - // Sample input data with nested JSON and various formatting needs - inputData := map[string]any{ - "user_data": `{"firstName": "john", "lastName": "doe", "age": "30", "salary": "75000.50", "isActive": "true"}`, - "metadata": `{"department": "engineering", "level": "senior", "skills": ["go", "python", "javascript"]}`, - } - - fmt.Println("🔗 Chaining multiple handlers to transform data...") - fmt.Printf("Input data: %+v\n", inputData) - - // Step 1: Parse JSON strings - jsonHandler := handlers.NewJSONHandler("json-step") - jsonConfig := dag.Payload{ - Data: map[string]any{ - "operation": "parse", - "fields": []string{"user_data", "metadata"}, - }, - } - jsonHandler.SetConfig(jsonConfig) - - step1Result := runHandler(jsonHandler, inputData, "Step 1: Parse JSON strings") - - if step1Result != nil { - // Step 2: Flatten the parsed nested data - flattenHandler := handlers.NewFlattenHandler("flatten-step") - flattenConfig := dag.Payload{ - Data: map[string]any{ - "operation": "flatten_nested_objects", - "separator": "_", - }, - } - flattenHandler.SetConfig(flattenConfig) - - step2Result := runHandler(flattenHandler, step1Result, "Step 2: Flatten nested objects") - - if step2Result != nil { - // Step 3: Format name fields to proper case - formatHandler := handlers.NewFormatHandler("format-step") - formatConfig := dag.Payload{ - Data: map[string]any{ - "format_type": "capitalize", - "fields": []string{"user_data_parsed_firstName", "user_data_parsed_lastName"}, - }, - } - formatHandler.SetConfig(formatConfig) - - step3Result := runHandler(formatHandler, step2Result, "Step 3: Format names to proper case") - - if step3Result != nil { - // Step 4: Rename fields to standard naming - fieldHandler := handlers.NewFieldHandler("rename-step") - renameConfig := dag.Payload{ - Data: map[string]any{ - "operation": "rename", - "mapping": map[string]any{ - "user_data_parsed_firstName": "first_name", - "user_data_parsed_lastName": "last_name", - "user_data_parsed_age": "age", - "user_data_parsed_salary": "salary", - "user_data_parsed_isActive": "is_active", - "metadata_parsed_department": "department", - "metadata_parsed_level": "level", - }, - }, - } - fieldHandler.SetConfig(renameConfig) - - step4Result := runHandler(fieldHandler, step3Result, "Step 4: Rename fields") - - if step4Result != nil { - // Step 5: Cast data types - dataHandler := handlers.NewDataHandler("cast-step") - castConfig := dag.Payload{ - Data: map[string]any{ - "operation": "type_cast", - "cast": map[string]any{ - "age": "int", - "salary": "float", - "is_active": "bool", - }, - }, - } - dataHandler.SetConfig(castConfig) - - finalResult := runHandler(dataHandler, step4Result, "Step 5: Cast data types") - printResult("🎉 Final chained transformation result", finalResult) - } - } - } - } -} diff --git a/examples/dedup.go b/examples/dedup.go deleted file mode 100644 index ca04122..0000000 --- a/examples/dedup.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "github.com/oarkflow/json" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - handler := tasks.SchedulerHandler - callback := tasks.SchedulerCallback - - // Initialize the pool with various parameters. - pool := mq.NewPool(3, - mq.WithTaskQueueSize(5), - mq.WithMaxMemoryLoad(1000), - mq.WithHandler(handler), - mq.WithPoolCallback(callback), - mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)), - ) - scheduler := mq.NewScheduler(pool) - scheduler.Start() - ctx := context.Background() - - // Example: Schedule an email task with deduplication. - // DedupKey here is set to the recipient's email (e.g., "user@example.com") to avoid duplicate email tasks. - emailPayload := json.RawMessage(`{"email": "user@example.com", "message": "Hello, Customer!"}`) - scheduler.AddTask(ctx, mq.NewTask("Email Task", emailPayload, "email", - mq.WithDedupKey("user@example.com"), - ), - mq.WithScheduleSpec("@every 1m"), // runs every minute for demonstration - mq.WithRecurring(), - ) - - scheduler.AddTask(ctx, mq.NewTask("Duplicate Email Task", emailPayload, "email", - mq.WithDedupKey("user@example.com"), - ), - mq.WithScheduleSpec("@every 1m"), - mq.WithRecurring(), - ) - - go func() { - for { - for _, task := range scheduler.ListScheduledTasks() { - fmt.Println("Scheduled.....", task) - } - time.Sleep(1 * time.Minute) - } - }() - - time.Sleep(10 * time.Minute) - -} diff --git a/examples/email/contact-form.html b/examples/email/contact-form.html deleted file mode 100644 index c3313c6..0000000 --- a/examples/email/contact-form.html +++ /dev/null @@ -1,190 +0,0 @@ - - - - - Contact Us - Email Notification System - - - - -
-

📧 Contact Us

-
Advanced Email Notification System with DAG Workflow
- -
-

🔄 Smart Routing: Our system automatically routes your message based on your user type - and preferences.

-
- -
-
- 📱 Instant Notifications
- Real-time email delivery -
-
- 🎯 Smart Targeting
- User-specific content -
-
- 🔒 Secure Processing
- Enterprise-grade security -
-
-
-
- {{form_groups}} -
- {{form_buttons}} -
-
-
-
- - - diff --git a/examples/email/error.html b/examples/email/error.html deleted file mode 100644 index 9935457..0000000 --- a/examples/email/error.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - Email Error - - - - -
-
-

Email Processing Error

- -
- {{error_message}} -
- - {{if error_field}} -
- 🎯 Error Field: {{error_field}}
- ⚡ Action Required: Please correct the highlighted field and try again.
- 💡 Tip: Make sure all required fields are properly filled out. -
- {{end}} - - {{if retry_suggested}} -
- ⚠️ Temporary Issue: This appears to be a temporary system issue. - Please try sending your message again in a few moments.
- 🔄 Auto-Retry: Our system will automatically retry failed deliveries. -
- {{end}} - -
- 🔄 Try Again - 📊 Check Status -
- -
- 🔄 DAG Error Handler | Email Notification Workflow Failed
- Our advanced routing system ensures reliable message delivery. -
-
- - - diff --git a/examples/email/success.html b/examples/email/success.html deleted file mode 100644 index f26e89d..0000000 --- a/examples/email/success.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - Message Sent Successfully - - - - -
-
-

Message Sent Successfully!

- -
{{email_status}}
- -
-
-
👤 Recipient
-
{{full_name}}
-
-
-
📧 Email Address
-
{{email}}
-
-
-
🆔 Email ID
-
{{email_id}}
-
-
-
⏰ Sent At
-
{{sent_at}}
-
-
-
📨 Email Type
-
{{email_type}}
-
-
-
👥 User Type
-
{{user_type}}
-
-
-
🚨 Priority
-
{{priority}}
-
-
-
🚚 Delivery
-
{{delivery_estimate}}
-
-
- -
-
📋 Subject:
-
- {{subject}} -
-
💬 Message ({{message_length}} chars):
-
- "{{message}}" -
-
- -
- 📧 Send Another Message - 📊 View Metrics -
- -
- 🔄 Workflow Details:
- Gateway: {{gateway}} | Template: {{email_template}} | Processed: {{processed_at}}
- This message was processed through our advanced DAG workflow system with conditional routing. -
-
- - - diff --git a/examples/email_notification_dag.go b/examples/email_notification_dag.go deleted file mode 100644 index a5598aa..0000000 --- a/examples/email_notification_dag.go +++ /dev/null @@ -1,460 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "regexp" - "strings" - "time" - - "github.com/oarkflow/json" - - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/handlers" - "github.com/oarkflow/mq/utils" - - "github.com/oarkflow/jet" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/consts" -) - -type ValidateLoginNode struct { - dag.Operation -} - -func (v *ValidateLoginNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: fmt.Errorf("invalid input data: %v", err), Ctx: ctx} - } - - username, _ := inputData["username"].(string) - password, _ := inputData["password"].(string) - - if username == "" || password == "" { - inputData["validation_error"] = "Username and password are required" - inputData["error_field"] = "credentials" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Simulate user validation - if username == "admin" && password == "password" { - inputData["validation_status"] = "success" - } else { - inputData["validation_error"] = "Invalid username or password" - inputData["error_field"] = "credentials" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - validatedData := map[string]any{ - "username": username, - "validated_at": time.Now().Format("2006-01-02 15:04:05"), - "validation_status": "success", - } - - bt, _ := json.Marshal(validatedData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"} -} - -func loginDAG() *dag.DAG { - flow := dag.NewDAG("Login Flow", "login-flow", func(taskID string, result mq.Result) { - fmt.Printf("Login flow completed for task %s: %s\n", taskID, string(result.Payload)) - }, mq.WithSyncMode(true), mq.WithLogger(nil)) - renderHTML := handlers.NewRenderHTMLNode("render-html") - renderHTML.Payload.Data = map[string]any{ - "schema_file": "login.json", - "template_file": "app/templates/basic.html", - } - flow.AddNode(dag.Page, "Login Form", "LoginForm", renderHTML, true) - flow.AddNode(dag.Function, "Validate Login", "ValidateLogin", &ValidateLoginNode{}) - flow.AddNode(dag.Page, "Error Page", "ErrorPage", &EmailErrorPageNode{}) - flow.AddNode(dag.Function, "Output", "Output", &handlers.OutputHandler{}) - flow.AddEdge(dag.Simple, "Validate Login", "LoginForm", "ValidateLogin") - flow.AddCondition("ValidateLogin", map[string]string{ - "invalid": "ErrorPage", - "valid": "Output", - }) - return flow -} - -func main() { - flow := dag.NewDAG("Email Notification System", "email-notification", func(taskID string, result mq.Result) { - fmt.Printf("Email notification workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content"))) - }, mq.WithSyncMode(true), mq.WithLogger(nil)) - - renderHTML := handlers.NewRenderHTMLNode("render-html") - renderHTML.Payload.Data = map[string]any{ - "schema_file": "schema.json", - "template_file": "app/templates/basic.html", - } - flow.AddDAGNode(dag.Page, "Check Login", "Login", loginDAG(), true) - flow.AddNode(dag.Page, "Contact Form", "ContactForm", renderHTML) - flow.AddNode(dag.Function, "Validate Contact Data", "ValidateContact", &ValidateContactNode{}) - flow.AddNode(dag.Function, "Check User Type", "CheckUserType", &CheckUserTypeNode{}) - flow.AddNode(dag.Function, "Send Welcome Email", "SendWelcomeEmail", &SendWelcomeEmailNode{}) - flow.AddNode(dag.Function, "Send Premium Email", "SendPremiumEmail", &SendPremiumEmailNode{}) - flow.AddNode(dag.Function, "Send Standard Email", "SendStandardEmail", &SendStandardEmailNode{}) - flow.AddNode(dag.Page, "Success Page", "SuccessPage", &SuccessPageNode{}) - flow.AddNode(dag.Page, "Error Page", "ErrorPage", &EmailErrorPageNode{}) - flow.AddEdge(dag.Simple, "Login to Contact", "Login.Output", "ContactForm") - // Define conditional flow - flow.AddEdge(dag.Simple, "Form to Validation", "ContactForm", "ValidateContact") - flow.AddCondition("ValidateContact", map[string]string{ - "valid": "CheckUserType", - "invalid": "ErrorPage", - }) - flow.AddCondition("CheckUserType", map[string]string{ - "new_user": "SendWelcomeEmail", - "premium_user": "SendPremiumEmail", - "standard_user": "SendStandardEmail", - }) - flow.AddCondition("SendWelcomeEmail", map[string]string{ - "sent": "SuccessPage", - "failed": "ErrorPage", - }) - flow.AddCondition("SendPremiumEmail", map[string]string{ - "sent": "SuccessPage", - "failed": "ErrorPage", - }) - flow.AddCondition("SendStandardEmail", map[string]string{ - "sent": "SuccessPage", - "failed": "ErrorPage", - }) - - // Start the flow - if flow.Error != nil { - panic(flow.Error) - } - - fmt.Println("Starting Email Notification DAG server on http://0.0.0.0:8084") - fmt.Println("Navigate to the URL to access the contact form") - flow.Start(context.Background(), "0.0.0.0:8084") -} - -// RenderHTMLNode - Page node with JSONSchema-based fields and custom HTML layout -// Usage: Pass JSONSchema and HTML layout to the node for dynamic form rendering and validation - -// ValidateContactNode - Validates contact form data -type ValidateContactNode struct { - dag.Operation -} - -func (v *ValidateContactNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{ - Error: fmt.Errorf("invalid input data: %v", err), - Ctx: ctx, - } - } - - // Extract form data - firstName, _ := inputData["first_name"].(string) - lastName, _ := inputData["last_name"].(string) - email, _ := inputData["email"].(string) - userType, _ := inputData["user_type"].(string) - priority, _ := inputData["priority"].(string) - subject, _ := inputData["subject"].(string) - message, _ := inputData["message"].(string) - - // Validate required fields - if firstName == "" { - inputData["validation_error"] = "First name is required" - inputData["error_field"] = "first_name" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if lastName == "" { - inputData["validation_error"] = "Last name is required" - inputData["error_field"] = "last_name" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if email == "" { - inputData["validation_error"] = "Email address is required" - inputData["error_field"] = "email" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Validate email format - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - if !emailRegex.MatchString(email) { - inputData["validation_error"] = "Please enter a valid email address" - inputData["error_field"] = "email" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if userType == "" { - inputData["validation_error"] = "Please select your user type" - inputData["error_field"] = "user_type" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if priority == "" { - inputData["validation_error"] = "Please select a priority level" - inputData["error_field"] = "priority" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if subject == "" { - inputData["validation_error"] = "Subject is required" - inputData["error_field"] = "subject" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if message == "" { - inputData["validation_error"] = "Message is required" - inputData["error_field"] = "message" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Check for spam patterns - spamPatterns := []string{"click here", "free money", "act now", "limited time"} - messageLower := strings.ToLower(message) - subjectLower := strings.ToLower(subject) - - for _, pattern := range spamPatterns { - if strings.Contains(messageLower, pattern) || strings.Contains(subjectLower, pattern) { - inputData["validation_error"] = "Message contains prohibited content" - inputData["error_field"] = "message" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - } - - // All validations passed - validatedData := map[string]any{ - "first_name": firstName, - "last_name": lastName, - "full_name": fmt.Sprintf("%s %s", firstName, lastName), - "email": email, - "user_type": userType, - "priority": priority, - "subject": subject, - "message": message, - "validated_at": time.Now().Format("2006-01-02 15:04:05"), - "validation_status": "success", - "message_length": len(message), - } - - bt, _ := json.Marshal(validatedData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"} -} - -// CheckUserTypeNode - Determines routing based on user type -type CheckUserTypeNode struct { - dag.Operation -} - -func (c *CheckUserTypeNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - userType, _ := inputData["user_type"].(string) - - // Add timestamp and additional metadata - inputData["processed_at"] = time.Now().Format("2006-01-02 15:04:05") - inputData["routing_decision"] = userType - - var conditionStatus string - switch userType { - case "new": - conditionStatus = "new_user" - inputData["email_template"] = "welcome" - case "premium": - conditionStatus = "premium_user" - inputData["email_template"] = "premium" - case "standard": - conditionStatus = "standard_user" - inputData["email_template"] = "standard" - default: - conditionStatus = "standard_user" - inputData["email_template"] = "standard" - } - - fmt.Printf("🔀 Routing decision: %s -> %s\n", userType, conditionStatus) - - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: conditionStatus} -} - -// Email sending nodes -type SendWelcomeEmailNode struct { - dag.Operation -} - -func (s *SendWelcomeEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return s.sendEmail(ctx, task, "Welcome to our platform! 🎉") -} - -type SendPremiumEmailNode struct { - dag.Operation -} - -func (s *SendPremiumEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return s.sendEmail(ctx, task, "Premium Support Response 💎") -} - -type SendStandardEmailNode struct { - dag.Operation -} - -func (s *SendStandardEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return s.sendEmail(ctx, task, "Thank you for contacting us ⭐") -} - -// Helper method for email sending -func (s *SendWelcomeEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - email, _ := inputData["email"].(string) - - // Simulate email sending delay - time.Sleep(300 * time.Millisecond) - - // Simulate occasional failures for demo purposes - timestamp := time.Now() - success := timestamp.Second()%15 != 0 // 93% success rate - - if !success { - errorData := inputData - errorData["email_status"] = "failed" - errorData["error_message"] = "Email gateway temporarily unavailable. Please try again." - errorData["sent_at"] = timestamp.Format("2006-01-02 15:04:05") - errorData["retry_suggested"] = true - - bt, _ := json.Marshal(errorData) - return mq.Result{ - Payload: bt, - Ctx: ctx, - ConditionStatus: "failed", - } - } - - // Generate mock email ID and response - emailID := fmt.Sprintf("EMAIL_%d_%s", timestamp.Unix(), email[0:3]) - - resultData := inputData - resultData["email_status"] = "sent" - resultData["email_id"] = emailID - resultData["email_type"] = emailType - resultData["sent_at"] = timestamp.Format("2006-01-02 15:04:05") - resultData["delivery_estimate"] = "Instant" - resultData["gateway"] = "MockEmail Gateway" - - fmt.Printf("📧 Email sent successfully! Type: %s, ID: %s, To: %s\n", emailType, emailID, email) - - bt, _ := json.Marshal(resultData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"} -} - -// Helper methods for other email nodes -func (s *SendPremiumEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result { - node := &SendWelcomeEmailNode{} - return node.sendEmail(ctx, task, emailType) -} - -func (s *SendStandardEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result { - node := &SendWelcomeEmailNode{} - return node.sendEmail(ctx, task, emailType) -} - -// SuccessPageNode - Shows successful email result -type SuccessPageNode struct { - dag.Operation -} - -func (s *SuccessPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - htmlTemplate, err := os.ReadFile("email/success.html") - if err != nil { - return mq.Result{Error: fmt.Errorf("failed to read success template: %v", err)} - } - - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(string(htmlTemplate), inputData) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - finalData := map[string]any{ - "html_content": rs, - "result": inputData, - "step": "success", - } - bt, _ := json.Marshal(finalData) - return mq.Result{Payload: bt, Ctx: ctx} -} - -// EmailErrorPageNode - Shows validation or sending errors -type EmailErrorPageNode struct { - dag.Operation -} - -func (e *EmailErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - // Determine error type and message - errorMessage, _ := inputData["validation_error"].(string) - errorField, _ := inputData["error_field"].(string) - emailError, _ := inputData["error_message"].(string) - - if errorMessage == "" && emailError != "" { - errorMessage = emailError - errorField = "email_sending" - } - if errorMessage == "" { - errorMessage = "An unknown error occurred" - } - - htmlTemplate, err := os.ReadFile("email/error.html") - if err != nil { - return mq.Result{Error: fmt.Errorf("failed to read error template: %v", err)} - } - - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - templateData := map[string]any{ - "error_message": errorMessage, - "error_field": errorField, - "retry_suggested": inputData["retry_suggested"], - } - - rs, err := parser.ParseTemplate(string(htmlTemplate), templateData) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - finalData := map[string]any{ - "html_content": rs, - "error_data": inputData, - "step": "error", - } - bt, _ := json.Marshal(finalData) - return mq.Result{Payload: bt, Ctx: ctx} -} diff --git a/examples/enhanced_pool_demo.go b/examples/enhanced_pool_demo.go deleted file mode 100644 index 55a80b5..0000000 --- a/examples/enhanced_pool_demo.go +++ /dev/null @@ -1,164 +0,0 @@ -// This is a demo showing the enhanced worker pool capabilities -// Run with: go run enhanced_pool_demo.go - -package main - -import ( - "context" - "fmt" - "log" - "time" - - "github.com/oarkflow/mq" -) - -// DemoHandler demonstrates a simple task handler -func DemoHandler(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("Processing task: %s\n", task.ID) - - // Simulate some work - time.Sleep(100 * time.Millisecond) - - return mq.Result{ - TaskID: task.ID, - Status: mq.Completed, - Payload: task.Payload, - } -} - -// DemoCallback demonstrates result processing -func DemoCallback(ctx context.Context, result mq.Result) error { - if result.Error != nil { - fmt.Printf("Task %s failed: %v\n", result.TaskID, result.Error) - } else { - fmt.Printf("Task %s completed successfully\n", result.TaskID) - } - return nil -} - -func main() { - fmt.Println("=== Enhanced Worker Pool Demo ===") - - // Create task storage - storage := mq.NewMemoryTaskStorage(1 * time.Hour) - - // Configure circuit breaker - circuitBreaker := mq.CircuitBreakerConfig{ - Enabled: true, - FailureThreshold: 5, - ResetTimeout: 30 * time.Second, - } - - // Create pool with enhanced configuration - pool := mq.NewPool(5, - mq.WithHandler(DemoHandler), - mq.WithPoolCallback(DemoCallback), - mq.WithTaskStorage(storage), - mq.WithBatchSize(3), - mq.WithMaxMemoryLoad(50*1024*1024), // 50MB - mq.WithCircuitBreaker(circuitBreaker), - ) - - fmt.Printf("Worker pool created with %d workers\n", 5) - - // Enqueue some tasks - fmt.Println("\n=== Enqueueing Tasks ===") - for i := 0; i < 10; i++ { - task := mq.NewTask( - fmt.Sprintf("demo-task-%d", i), - []byte(fmt.Sprintf(`{"message": "Hello from task %d", "timestamp": "%s"}`, i, time.Now().Format(time.RFC3339))), - "demo", - ) - - // Add some tasks with higher priority - priority := 1 - if i%3 == 0 { - priority = 5 // Higher priority - } - - err := pool.EnqueueTask(context.Background(), task, priority) - if err != nil { - log.Printf("Failed to enqueue task %d: %v", i, err) - } else { - fmt.Printf("Enqueued task %s with priority %d\n", task.ID, priority) - } - } - - // Monitor progress - fmt.Println("\n=== Monitoring Progress ===") - for i := 0; i < 10; i++ { - time.Sleep(500 * time.Millisecond) - - metrics := pool.FormattedMetrics() - health := pool.GetHealthStatus() - - fmt.Printf("Progress: %d/%d completed, %d errors, Queue: %d, Healthy: %v\n", - metrics.CompletedTasks, - metrics.TotalTasks, - metrics.ErrorCount, - health.QueueDepth, - health.IsHealthy, - ) - - if metrics.CompletedTasks >= 10 { - break - } - } - - // Display final metrics - fmt.Println("\n=== Final Metrics ===") - finalMetrics := pool.FormattedMetrics() - fmt.Printf("Total Tasks: %d\n", finalMetrics.TotalTasks) - fmt.Printf("Completed: %d\n", finalMetrics.CompletedTasks) - fmt.Printf("Errors: %d\n", finalMetrics.ErrorCount) - fmt.Printf("Memory Used: %s\n", finalMetrics.CurrentMemoryUsed) - fmt.Printf("Execution Time: %s\n", finalMetrics.CumulativeExecution) - fmt.Printf("Average Execution: %s\n", finalMetrics.AverageExecution) - - // Test dynamic worker scaling - fmt.Println("\n=== Dynamic Scaling Demo ===") - fmt.Printf("Current workers: %d\n", 5) - - pool.AdjustWorkerCount(8) - fmt.Printf("Scaled up to: %d workers\n", 8) - - time.Sleep(1 * time.Second) - - pool.AdjustWorkerCount(3) - fmt.Printf("Scaled down to: %d workers\n", 3) - - // Test health status - fmt.Println("\n=== Health Status ===") - health := pool.GetHealthStatus() - fmt.Printf("Health Status: %+v\n", health) - - // Test DLQ (simulate some failures) - fmt.Println("\n=== Dead Letter Queue Demo ===") - dlqTasks := pool.DLQ().Tasks() - fmt.Printf("Tasks in DLQ: %d\n", len(dlqTasks)) - - // Test configuration update - fmt.Println("\n=== Configuration Update Demo ===") - currentConfig := pool.GetCurrentConfig() - fmt.Printf("Current batch size: %d\n", currentConfig.BatchSize) - - newConfig := currentConfig - newConfig.BatchSize = 5 - newConfig.NumberOfWorkers = 4 - - err := pool.UpdateConfig(&newConfig) - if err != nil { - log.Printf("Failed to update config: %v", err) - } else { - fmt.Printf("Updated batch size to: %d\n", newConfig.BatchSize) - fmt.Printf("Updated worker count to: %d\n", newConfig.NumberOfWorkers) - } - - // Graceful shutdown - fmt.Println("\n=== Graceful Shutdown ===") - fmt.Println("Shutting down pool...") - pool.Stop() - fmt.Println("Pool shutdown completed") - - fmt.Println("\n=== Demo Complete ===") -} diff --git a/examples/form.go b/examples/form.go deleted file mode 100644 index b63b356..0000000 --- a/examples/form.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/oarkflow/json" - - "github.com/oarkflow/mq/dag" - - "github.com/oarkflow/jet" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/consts" -) - -func main() { - flow := dag.NewDAG("Multi-Step Form", "multi-step-form", func(taskID string, result mq.Result) { - fmt.Printf("Final result for task %s: %s\n", taskID, string(result.Payload)) - }) - flow.AddNode(dag.Page, "Form Step1", "FormStep1", &FormStep1{}) - flow.AddNode(dag.Page, "Form Step2", "FormStep2", &FormStep2{}) - flow.AddNode(dag.Page, "Form Result", "FormResult", &FormResult{}) - - // Define edges - flow.AddEdge(dag.Simple, "Form Step1", "FormStep1", "FormStep2") - flow.AddEdge(dag.Simple, "Form Step2", "FormStep2", "FormResult") - - // Start the flow - if flow.Error != nil { - panic(flow.Error) - } - flow.Start(context.Background(), "0.0.0.0:8082") -} - -type FormStep1 struct { - dag.Operation -} - -func (p *FormStep1) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - bt := []byte(` - - - -
- - - - - -
- - - -
- {{ if show_voting_controls }} - - - - {{ else }} -

You are not eligible to vote.

- {{ end }} -
- - -`) - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - inputData["task_id"] = ctx.Value("task_id") - rs, err := parser.ParseTemplate(string(bt), inputData) - if err != nil { - fmt.Println("FormStep2", inputData) - return mq.Result{Error: err, Ctx: ctx} - } - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - inputData["html_content"] = rs - bt, _ = json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx} -} - -type FormResult struct { - dag.Operation -} - -func (p *FormResult) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - // Load HTML template for results - bt := []byte(` - - - -

Form Summary

-

Name: {{ name }}

-

Age: {{ age }}

-{{ if register_vote }} -

You have registered to vote!

-{{ else }} -

You did not register to vote.

-{{ end }} - - - - -`) - var inputData map[string]any - if task.Payload != nil { - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - } - if inputData != nil { - if isEligible, ok := inputData["register_vote"].(string); ok { - inputData["register_vote"] = isEligible - } else { - inputData["register_vote"] = false - } - } - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(string(bt), inputData) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - inputData["html_content"] = rs - bt, _ = json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx} -} diff --git a/examples/form.json b/examples/form.json deleted file mode 100644 index ef87dab..0000000 --- a/examples/form.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "Multi-Step Form", - "key": "multi-step-form", - "nodes": [ - { - "name": "Form Step1", - "id": "FormStep1", - "node": "Page", - "data": { - "template": "
", - "mapping": {} - }, - "first_node": true - }, - { - "name": "Form Step2", - "id": "FormStep2", - "node": "Page", - "data": { - "template": "
{{ if show_voting_controls }}{{ else }}

You are not eligible to vote.

{{ end }}
", - "mapping": {} - } - }, - { - "name": "Form Result", - "id": "FormResult", - "node": "Page", - "data": { - "template": "

Form Summary

Name: {{ name }}

Age: {{ age }}

{{ if register_vote }}

You have registered to vote!

{{ else }}

You did not register to vote.

{{ end }}", - "mapping": {} - } - } - ], - "edges": [ - { - "source": "FormStep1", - "target": ["FormStep2"] - }, - { - "source": "FormStep2", - "target": ["FormResult"] - } - ] -} diff --git a/examples/generate_cert.go b/examples/generate_cert.go deleted file mode 100644 index 77b368d..0000000 --- a/examples/generate_cert.go +++ /dev/null @@ -1,468 +0,0 @@ -package main - -import ( - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "errors" - "fmt" - "log" - "math/big" - "net" - "os" - "time" - - "github.com/oarkflow/json" -) - -func main() { - - caCertDER, caPrivateKey := generateCA("P384", "My Secure CA") - saveCertificate("ca.crt", caCertDER) - signFileContent("ca.crt", caPrivateKey) - - caCert, err := x509.ParseCertificate(caCertDER) - if err != nil { - log.Fatal("Failed to parse CA certificate:", err) - } - - serverCertDER, serverKey := generateServerCert(caCert, caPrivateKey, "P256", "server.example.com", - []string{"localhost", "server.example.com"}, []net.IP{net.ParseIP("127.0.0.1")}) - saveCertificate("server.crt", serverCertDER) - signFileContent("server.crt", caPrivateKey) - - clientCertDER, clientKey := generateClientCert(caCert, caPrivateKey, "client-user") - saveCertificate("client.crt", clientCertDER) - signFileContent("client.crt", caPrivateKey) - - codeSignCertDER, codeSignKey := generateCodeSigningCert(caCert, caPrivateKey, 2048, "file-signer") - saveCertificate("code_sign.crt", codeSignCertDER) - signFileContent("code_sign.crt", caPrivateKey) - - revokedCerts := []pkix.RevokedCertificate{ - {SerialNumber: big.NewInt(1), RevocationTime: time.Now()}, - } - generateCRL(caCert, caPrivateKey, revokedCerts) - - if err := validateClientCert("client.crt", "ca.crt"); err != nil { - log.Fatal("Client certificate validation failed:", err) - } - - log.Println("All certificates generated and signed successfully.") - - dss := NewDigitalSignatureService(clientKey) - - plainText := "The quick brown fox jumps over the lazy dog." - textSig, err := dss.SignText(plainText) - if err != nil { - log.Fatal("Failed to sign plain text:", err) - } - if err := dss.VerifyText(plainText, textSig); err != nil { - log.Fatal("Plain text signature verification failed:", err) - } - log.Println("Plain text signature verified.") - - jsonData := map[string]interface{}{ - "name": "Alice", - "age": 30, - "premium": true, - } - jsonSig, err := dss.SignJSON(jsonData) - if err != nil { - log.Fatal("Failed to sign JSON data:", err) - } - fmt.Println(string(jsonSig)) - if err := dss.VerifyJSON(jsonData, jsonSig); err != nil { - log.Fatal("JSON signature verification failed:", err) - } - log.Println("JSON signature verified.") - - _ = serverKey - _ = codeSignKey -} - -type DigitalSignatureService struct { - Signer crypto.Signer - PublicKey crypto.PublicKey -} - -func NewDigitalSignatureService(signer crypto.Signer) *DigitalSignatureService { - return &DigitalSignatureService{ - Signer: signer, - PublicKey: signer.Public(), - } -} - -func (dss *DigitalSignatureService) SignData(data []byte) ([]byte, error) { - switch dss.Signer.(type) { - case ed25519.PrivateKey: - return dss.Signer.Sign(rand.Reader, data, crypto.Hash(0)) - default: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - return dss.Signer.Sign(rand.Reader, hashed, crypto.SHA256) - } -} - -func (dss *DigitalSignatureService) VerifyData(data, signature []byte) error { - switch pub := dss.PublicKey.(type) { - case ed25519.PublicKey: - if ed25519.Verify(pub, data, signature) { - return nil - } - return errors.New("ed25519 signature verification failed") - case *rsa.PublicKey: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed, signature) - case *ecdsa.PublicKey: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - var sig struct { - R, S *big.Int - } - if _, err := asn1.Unmarshal(signature, &sig); err != nil { - return err - } - if ecdsa.Verify(pub, hashed, sig.R, sig.S) { - return nil - } - return errors.New("ecdsa signature verification failed") - default: - return errors.New("unsupported public key type") - } -} - -func (dss *DigitalSignatureService) SignText(text string) ([]byte, error) { - return dss.SignData([]byte(text)) -} - -func (dss *DigitalSignatureService) VerifyText(text string, signature []byte) error { - return dss.VerifyData([]byte(text), signature) -} - -func (dss *DigitalSignatureService) SignJSON(v interface{}) ([]byte, error) { - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - return dss.SignData(b) -} - -func (dss *DigitalSignatureService) VerifyJSON(v interface{}, signature []byte) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - return dss.VerifyData(b, signature) -} - -func generateCA(curveName string, commonName string) ([]byte, crypto.Signer) { - privKey, err := generatePrivateKey("ECDSA", curveName) - if err != nil { - log.Fatal("CA key generation failed:", err) - } - subject := pkix.Name{ - CommonName: commonName, - Organization: []string{"Secure CA Org"}, - Country: []string{"US"}, - } - template := x509.Certificate{ - SerialNumber: randomSerial(), - Subject: subject, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - BasicConstraintsValid: true, - MaxPathLenZero: true, - } - certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, privKey.Public(), privKey) - if err != nil { - log.Fatal("CA cert creation failed:", err) - } - - return certBytes, privKey -} - -func generateServerCert(caCert *x509.Certificate, caKey crypto.Signer, keyType string, commonName string, dnsNames []string, ips []net.IP) ([]byte, crypto.Signer) { - privKey, err := generatePrivateKey("ECDSA", keyType) - if err != nil { - log.Fatal("Server key generation failed:", err) - } - template := x509.Certificate{ - SerialNumber: randomSerial(), - Subject: pkix.Name{ - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - DNSNames: dnsNames, - IPAddresses: ips, - } - certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey) - if err != nil { - log.Fatal("Server cert creation failed:", err) - } - return certBytes, privKey -} - -func generateClientCert(caCert *x509.Certificate, caKey crypto.Signer, commonName string) ([]byte, crypto.Signer) { - privKey, err := generatePrivateKey("Ed25519", nil) - if err != nil { - log.Fatal("Client key generation failed:", err) - } - template := x509.Certificate{ - SerialNumber: randomSerial(), - Subject: pkix.Name{ - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey) - if err != nil { - log.Fatal("Client cert creation failed:", err) - } - return certBytes, privKey -} - -func generateCodeSigningCert(caCert *x509.Certificate, caKey crypto.Signer, rsaBits int, commonName string) ([]byte, crypto.Signer) { - privKey, err := generatePrivateKey("RSA", rsaBits) - if err != nil { - log.Fatal("Code signing key generation failed:", err) - } - template := x509.Certificate{ - SerialNumber: randomSerial(), - Subject: pkix.Name{ - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(5, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - } - certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey) - if err != nil { - log.Fatal("Code signing cert creation failed:", err) - } - return certBytes, privKey -} - -func generateCRL(caCert *x509.Certificate, caKey crypto.Signer, revoked []pkix.RevokedCertificate) { - crlTemplate := &x509.RevocationList{ - SignatureAlgorithm: caCert.SignatureAlgorithm, - RevokedCertificates: revoked, - Number: randomSerial(), - ThisUpdate: time.Now(), - NextUpdate: time.Now().AddDate(0, 1, 0), - Issuer: caCert.Subject, - } - crlBytes, err := x509.CreateRevocationList(rand.Reader, crlTemplate, caCert, caKey) - if err != nil { - log.Fatal("CRL creation failed:", err) - } - saveCRL("ca.crl", crlBytes) - signFileContent("ca.crl", caKey) -} - -func generatePrivateKey(algo string, param interface{}) (crypto.Signer, error) { - switch algo { - case "RSA": - bits, ok := param.(int) - if !ok { - return nil, fmt.Errorf("invalid RSA parameter") - } - return rsa.GenerateKey(rand.Reader, bits) - case "ECDSA": - curveName, ok := param.(string) - if !ok { - return nil, fmt.Errorf("invalid ECDSA parameter") - } - var curve elliptic.Curve - switch curveName { - case "P224": - curve = elliptic.P224() - case "P256": - curve = elliptic.P256() - case "P384": - curve = elliptic.P384() - case "P521": - curve = elliptic.P521() - default: - return nil, fmt.Errorf("unsupported curve") - } - return ecdsa.GenerateKey(curve, rand.Reader) - case "Ed25519": - _, priv, err := ed25519.GenerateKey(rand.Reader) - return priv, err - default: - return nil, fmt.Errorf("unsupported algorithm") - } -} - -func saveCertificate(filename string, cert []byte) { - err := os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert, - }), 0644) - if err != nil { - log.Fatal("Failed to save certificate:", err) - } -} - -func savePrivateKey(filename string, key crypto.Signer) { - keyBytes, err := x509.MarshalPKCS8PrivateKey(key) - if err != nil { - log.Fatal("Failed to marshal private key:", err) - } - err = os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: keyBytes, - }), 0600) - if err != nil { - log.Fatal("Failed to save private key:", err) - } -} - -func saveCRL(filename string, crl []byte) { - err := os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{ - Type: "X509 CRL", - Bytes: crl, - }), 0644) - if err != nil { - log.Fatal("Failed to save CRL:", err) - } -} - -func signFileContent(filename string, signer crypto.Signer) { - data, err := os.ReadFile(filename) - if err != nil { - log.Fatal("Failed to read file for signing:", err) - } - sig, err := signData(data, signer) - if err != nil { - log.Fatal("Failed to sign file content:", err) - } - err = os.WriteFile(filename+".sig", sig, 0644) - if err != nil { - log.Fatal("Failed to save signature file:", err) - } -} - -func signData(data []byte, key crypto.Signer) ([]byte, error) { - switch key.(type) { - case ed25519.PrivateKey: - return key.Sign(rand.Reader, data, crypto.Hash(0)) - default: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - return key.Sign(rand.Reader, hashed, crypto.SHA256) - } -} - -func verifyDataSignature(data, signature []byte, pub crypto.PublicKey) error { - switch pub := pub.(type) { - case ed25519.PublicKey: - if ed25519.Verify(pub, data, signature) { - return nil - } - return errors.New("ed25519 signature verification failed") - case *rsa.PublicKey: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed, signature) - case *ecdsa.PublicKey: - h := sha256.New() - h.Write(data) - hashed := h.Sum(nil) - var sig struct { - R, S *big.Int - } - if _, err := asn1.Unmarshal(signature, &sig); err != nil { - return err - } - if ecdsa.Verify(pub, hashed, sig.R, sig.S) { - return nil - } - return errors.New("ecdsa signature verification failed") - default: - return errors.New("unsupported public key type") - } -} - -func verifyFileContentSignature(filename, sigFilename string, pub crypto.PublicKey) error { - data, err := os.ReadFile(filename) - if err != nil { - return fmt.Errorf("failed to read file: %w", err) - } - sig, err := os.ReadFile(sigFilename) - if err != nil { - return fmt.Errorf("failed to read signature file: %w", err) - } - return verifyDataSignature(data, sig, pub) -} - -func validateClientCert(clientCertPath, caCertPath string) error { - caCertBytes, err := os.ReadFile(caCertPath) - if err != nil { - return err - } - block, _ := pem.Decode(caCertBytes) - if block == nil { - return fmt.Errorf("failed to decode CA cert PEM") - } - caCert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return err - } - clientCertBytes, err := os.ReadFile(clientCertPath) - if err != nil { - return err - } - block, _ = pem.Decode(clientCertBytes) - if block == nil { - return fmt.Errorf("failed to decode client cert PEM") - } - clientCert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return err - } - opts := x509.VerifyOptions{ - Roots: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - opts.Roots.AddCert(caCert) - if _, err = clientCert.Verify(opts); err != nil { - return fmt.Errorf("certificate chain verification failed: %w", err) - } - if err = verifyFileContentSignature(clientCertPath, clientCertPath+".sig", caCert.PublicKey); err != nil { - return fmt.Errorf("file signature verification failed: %w", err) - } - return nil -} - -func randomSerial() *big.Int { - serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - log.Fatal("failed to generate serial number:", err) - } - return serial -} diff --git a/examples/hmac.go b/examples/hmac.go deleted file mode 100644 index e8e0038..0000000 --- a/examples/hmac.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "crypto/rand" - "encoding/hex" - "fmt" -) - -func generateHMACKey() ([]byte, error) { - key := make([]byte, 32) // 32 bytes = 256 bits - _, err := rand.Read(key) - if err != nil { - return nil, err - } - return key, nil -} - -func main() { - hmacKey, err := generateHMACKey() - if err != nil { - fmt.Println("Error generating HMAC key:", err) - return - } - - fmt.Println("HMAC Key (hex):", hex.EncodeToString(hmacKey)) -} diff --git a/examples/json.go b/examples/json.go deleted file mode 100644 index dc115c7..0000000 --- a/examples/json.go +++ /dev/null @@ -1,166 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/oarkflow/jet" - "github.com/oarkflow/json" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/consts" - "github.com/oarkflow/mq/dag" -) - -type Data struct { - Template string `json:"template"` - Mapping map[string]string `json:"mapping"` - AdditionalData map[string]any `json:"additional_data"` -} - -type Node struct { - Name string `json:"name"` - ID string `json:"id"` - Node string `json:"node"` - Data Data `json:"data"` - FirstNode bool `json:"first_node"` -} - -type Edge struct { - Source string `json:"source"` - Target []string `json:"target"` -} - -type Handler struct { - Name string `json:"name"` - Key string `json:"key"` - Nodes []Node `json:"nodes"` - Edges []Edge `json:"edges"` -} - -func CreateDAGFromJSON(config string) (*dag.DAG, error) { - var handler Handler - err := json.Unmarshal([]byte(config), &handler) - if err != nil { - return nil, fmt.Errorf("failed to parse JSON: %v", err) - } - - flow := dag.NewDAG(handler.Name, handler.Key, func(taskID string, result mq.Result) { - fmt.Printf("Final result for task %s: %s\n", taskID, string(result.Payload)) - }) - - nodeMap := make(map[string]mq.Processor) - - for _, node := range handler.Nodes { - op := &HTMLProcessor{ - Operation: dag.Operation{ - Payload: dag.Payload{ - Mapping: node.Data.Mapping, - Data: node.Data.AdditionalData, - }, - }, - Template: node.Data.Template, - } - nodeMap[node.ID] = op - flow.AddNode(dag.Page, node.Name, node.ID, op, node.FirstNode) - } - - for _, edge := range handler.Edges { - for _, target := range edge.Target { - flow.AddEdge(dag.Simple, edge.Source, edge.Source, target) - } - } - - if flow.Error != nil { - return nil, flow.Error - } - return flow, nil -} - -type HTMLProcessor struct { - dag.Operation - Template string -} - -func (p *HTMLProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - data := map[string]interface{}{ - "task_id": ctx.Value("task_id"), - } - p.Debug(ctx, task) - for key, value := range p.Payload.Mapping { - data[key] = value - } - - rs, err := parser.ParseTemplate(p.Template, data) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - response := map[string]interface{}{ - "html_content": rs, - } - payload, _ := json.Marshal(response) - - return mq.Result{Payload: payload, Ctx: ctx} -} - -func main() { - // JSON configuration - jsonConfig := `{ - "name": "Multi-Step Form", - "key": "multi-step-form", - "nodes": [ - { - "name": "Form Step1", - "id": "FormStep1", - "node": "Page", - "data": { - "additional_data": {"debug": true}, - "template": "\n\n
\n \n \n \n \n \n
\n\n", - "mapping": {} - }, - "first_node": true - }, - { - "name": "Form Step2", - "id": "FormStep2", - "node": "Page", - "data": { - "template": "\n\n
\n {{ if show_voting_controls }}\n \n \n \n {{ else }}\n

You are not eligible to vote.

\n {{ end }}\n
\n\n", - "mapping": { - "show_voting_controls": "conditional" - } - } - }, - { - "name": "Form Result", - "id": "FormResult", - "node": "Page", - "data": { - "template": "\n\n

Form Summary

\n

Name: {{ name }}

\n

Age: {{ age }}

\n{{ if register_vote }}\n

You have registered to vote!

\n{{ else }}\n

You did not register to vote.

\n{{ end }}\n\n", - "mapping": {} - } - } - ], - "edges": [ - { - "source": "FormStep1", - "target": ["FormStep2"] - }, - { - "source": "FormStep2", - "target": ["FormResult"] - } - ] -}` - - flow, err := CreateDAGFromJSON(jsonConfig) - if err != nil { - log.Fatalf("Error creating DAG: %v", err) - } - - flow.Start(context.Background(), "0.0.0.0:8082") -} diff --git a/examples/login.json b/examples/login.json deleted file mode 100644 index 8f9cec1..0000000 --- a/examples/login.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "type": "object", - "properties": { - "username": { - "type": "string", - "title": "Username or Email", - "order": 1, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "name": "username", - "placeholder": "Enter your username or email" - } - }, - "password": { - "type": "string", - "title": "Password", - "order": 2, - "ui": { - "element": "input", - "type": "password", - "class": "form-group", - "name": "password", - "placeholder": "Enter your password" - } - }, - "remember_me": { - "type": "boolean", - "title": "Remember Me", - "order": 3, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-check", - "name": "remember_me" - } - } - }, - "required": [ "username", "password" ], - "form": { - "class": "form-horizontal", - "action": "/process?task_id={{task_id}}&next=true", - "method": "POST", - "enctype": "application/x-www-form-urlencoded", - "groups": [ - { - "title": "Login Credentials", - "fields": [ "username", "password", "remember_me" ] - } - ], - "submit": { - "type": "submit", - "label": "Log In", - "class": "btn btn-primary" - }, - "reset": { - "type": "reset", - "label": "Clear", - "class": "btn btn-secondary" - } - } -} diff --git a/examples/minimal_admin/main.go b/examples/minimal_admin/main.go deleted file mode 100644 index 6355e89..0000000 --- a/examples/minimal_admin/main.go +++ /dev/null @@ -1,121 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "time" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/logger" -) - -// Example: Proper timeout patterns for different connection types -func demonstrateTimeoutPatterns() { - fmt.Println("\n=== Connection Timeout Patterns ===") - - // ✅ GOOD: No I/O timeout for persistent broker-consumer connections - fmt.Println("✅ Broker-Consumer: NO I/O timeouts (persistent connection)") - fmt.Println(" - Uses TCP keep-alive for network detection") - fmt.Println(" - Uses context cancellation for graceful shutdown") - fmt.Println(" - Connection stays open indefinitely waiting for messages") - - // ✅ GOOD: Timeout for connection establishment - fmt.Println("✅ Connection Setup: WITH timeout (initial connection)") - fmt.Println(" - 10-30 second timeout for initial TCP handshake") - fmt.Println(" - Prevents hanging on unreachable servers") - - // ✅ GOOD: Timeout for individual task processing - fmt.Println("✅ Task Processing: WITH timeout (individual operations)") - fmt.Println(" - 30 second timeout per task (configurable)") - fmt.Println(" - Prevents individual tasks from hanging forever") - - // ❌ BAD: I/O timeout on persistent connections - fmt.Println("❌ Persistent I/O: NO timeouts on read/write operations") - fmt.Println(" - Would cause 'i/o timeout' errors on idle connections") - fmt.Println(" - Breaks the persistent connection model") - - fmt.Println("\n💡 Key Principle:") - fmt.Println(" - Connection timeouts: Only for establishment") - fmt.Println(" - I/O timeouts: Only for request/response patterns") - fmt.Println(" - Persistent connections: Use keep-alive + context cancellation") -} - -func main() { - fmt.Println("=== Minimal Admin Server Test ===") - - // Demonstrate proper timeout patterns - demonstrateTimeoutPatterns() - - // Create logger - lg := logger.NewDefaultLogger() - fmt.Println("\n✅ Logger created") - - // Create broker with NO I/O timeouts for persistent connections - broker := mq.NewBroker(mq.WithLogger(lg), mq.WithBrokerURL(":8081")) - fmt.Println("✅ Broker created (NO I/O timeouts - persistent connections)") - - // Start broker - ctx := context.Background() - fmt.Println("🚀 Starting broker...") - - // Start broker in goroutine since it blocks - go func() { - if err := broker.Start(ctx); err != nil { - log.Printf("❌ Broker error: %v", err) - } - }() - - // Give broker time to start - time.Sleep(500 * time.Millisecond) - fmt.Println("✅ Broker started") - defer broker.Close() - - // Create admin server - fmt.Println("🔧 Creating admin server...") - adminServer := mq.NewAdminServer(broker, ":8090", lg) - fmt.Println("✅ Admin server created") - - // Start admin server - fmt.Println("🚀 Starting admin server...") - if err := adminServer.Start(); err != nil { - log.Fatalf("❌ Failed to start admin server: %v", err) - } - defer adminServer.Stop() - fmt.Println("✅ Admin server started") - - // Wait and test - fmt.Println("⏳ Waiting 2 seconds...") - time.Sleep(2 * time.Second) - - fmt.Println("🔍 Testing connectivity...") - - // ✅ GOOD: HTTP client WITH timeout (request/response pattern) - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get("http://localhost:8090/api/admin/health") - if err != nil { - fmt.Printf("❌ Connection failed: %v\n", err) - } else { - fmt.Printf("✅ Connection successful! Status: %d\n", resp.StatusCode) - resp.Body.Close() - } - - fmt.Println("\n🌐 Admin Dashboard: http://localhost:8090/admin") - fmt.Println("📊 Health API: http://localhost:8090/api/admin/health") - fmt.Println("📡 Broker API: http://localhost:8090/api/admin/broker") - fmt.Println("📋 Queue API: http://localhost:8090/api/admin/queues") - - fmt.Println("\n💡 Connection Patterns Demonstrated:") - fmt.Println(" 🔗 Broker ↔ Consumer: Persistent (NO I/O timeouts)") - fmt.Println(" 🌐 HTTP Client ↔ Admin: Request/Response (WITH timeouts)") - fmt.Println(" ⚡ WebSocket ↔ Dashboard: Persistent (NO I/O timeouts)") - - fmt.Println("\n⚠️ Server running - Press Ctrl+C to stop") - fmt.Println(" - Broker connections will remain persistent") - fmt.Println(" - No 'i/o timeout' errors should occur") - fmt.Println(" - TCP keep-alive handles network detection") - - // Keep running - select {} -} diff --git a/examples/minimal_admin/static/admin/README.md b/examples/minimal_admin/static/admin/README.md deleted file mode 100644 index bb5c99f..0000000 --- a/examples/minimal_admin/static/admin/README.md +++ /dev/null @@ -1,284 +0,0 @@ -# MQ Admin Dashboard - -The MQ Admin Dashboard provides a comprehensive web-based interface for managing and monitoring your MQ broker, queues, consumers, and worker pools. It offers real-time metrics, control capabilities, and health monitoring similar to RabbitMQ's management interface. - -## Features - -### 🌟 **Comprehensive Dashboard** -- **Real-time Monitoring**: Live charts showing throughput, queue depth, and system metrics -- **Broker Management**: Monitor broker status, uptime, and configuration -- **Queue Management**: View queue depths, consumer counts, and flush capabilities -- **Consumer Control**: Monitor consumer status, pause/resume operations, and performance metrics -- **Worker Pool Management**: Track pool status, worker counts, and memory usage -- **Health Checks**: System health monitoring with detailed status reports - -### 📊 **Real-time Visualizations** -- Interactive charts powered by Chart.js -- Live WebSocket updates for real-time data -- Throughput history and trend analysis -- System resource monitoring (CPU, Memory, Goroutines) - -### 🎛️ **Management Controls** -- Pause/Resume consumers -- Adjust worker pool settings -- Flush queues -- Restart/Stop broker operations -- Configuration management - -## Quick Start - -### 1. Run the Admin Demo - -```bash -cd examples/admin -go run main.go -``` - -This will start: -- MQ Broker (background) -- Admin Dashboard on http://localhost:8090/admin -- Sample task simulation for demonstration - -### 2. Access the Dashboard - -Open your browser and navigate to: -``` -http://localhost:8090/admin -``` - -### 3. Explore the Interface - -The dashboard consists of several tabs: - -- **📈 Overview**: High-level metrics and system status -- **🔧 Broker**: Broker configuration and control -- **📋 Queues**: Queue monitoring and management -- **👥 Consumers**: Consumer status and control -- **🏊 Pools**: Worker pool monitoring -- **❤️ Monitoring**: Health checks and system metrics - -## Integration in Your Application - -### Basic Usage - -```go -package main - -import ( - "context" - "log" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/logger" -) - -func main() { - // Create logger - lg := logger.NewDefaultLogger() - - // Create broker - broker := mq.NewBroker(mq.WithLogger(lg)) - - // Start broker - ctx := context.Background() - if err := broker.Start(ctx); err != nil { - log.Fatalf("Failed to start broker: %v", err) - } - defer broker.Close() - - // Create admin server - adminServer := mq.NewAdminServer(broker, ":8090", lg) - if err := adminServer.Start(); err != nil { - log.Fatalf("Failed to start admin server: %v", err) - } - defer adminServer.Stop() - - // Your application logic here... -} -``` - -### Configuration Options - -The admin server can be configured with different options: - -```go -// Custom port -adminServer := mq.NewAdminServer(broker, ":9090", lg) - -// With custom logger -customLogger := logger.NewDefaultLogger() -adminServer := mq.NewAdminServer(broker, ":8090", customLogger) -``` - -## API Endpoints - -The admin server exposes several REST API endpoints: - -### Core Endpoints -- `GET /admin` - Main dashboard interface -- `GET /api/admin/metrics` - Real-time metrics -- `GET /api/admin/broker` - Broker information -- `GET /api/admin/queues` - Queue status -- `GET /api/admin/consumers` - Consumer information -- `GET /api/admin/pools` - Worker pool status -- `GET /api/admin/health` - Health checks - -### Control Endpoints -- `POST /api/admin/broker/restart` - Restart broker -- `POST /api/admin/broker/stop` - Stop broker -- `POST /api/admin/queues/flush` - Flush all queues - -### Example API Usage - -```bash -# Get current metrics -curl http://localhost:8090/api/admin/metrics - -# Get broker status -curl http://localhost:8090/api/admin/broker - -# Flush all queues -curl -X POST http://localhost:8090/api/admin/queues/flush -``` - -## Dashboard Features - -### 🎨 **Modern UI Design** -- Responsive design with Tailwind CSS -- Clean, intuitive interface -- Dark/light theme support -- Mobile-friendly layout - -### 📊 **Real-time Charts** -- Live throughput monitoring -- Queue depth trends -- System resource usage -- Error rate tracking - -### ⚡ **Interactive Controls** -- Consumer pause/resume buttons -- Pool configuration modals -- Queue management tools -- Real-time status updates - -### 🔄 **WebSocket Integration** -- Live data updates without page refresh -- Real-time event streaming -- Automatic reconnection -- Low-latency monitoring - -## File Structure - -The admin interface consists of: - -``` -static/admin/ -├── index.html # Main dashboard interface -├── css/ -│ └── admin.css # Custom styling -└── js/ - └── admin.js # Dashboard JavaScript logic -``` - -## Metrics and Monitoring - -### System Metrics -- **Throughput**: Messages processed per second -- **Queue Depth**: Number of pending messages -- **Active Consumers**: Currently running consumers -- **Error Rate**: Failed message percentage -- **Memory Usage**: System memory consumption -- **CPU Usage**: System CPU utilization - -### Queue Metrics -- Message count per queue -- Consumer count per queue -- Processing rate per queue -- Error count per queue - -### Consumer Metrics -- Messages processed -- Error count -- Last activity timestamp -- Configuration parameters - -### Pool Metrics -- Active workers -- Queue size -- Memory load -- Task distribution - -## Customization - -### Styling -The interface uses Tailwind CSS and can be customized by modifying `static/admin/css/admin.css`. - -### JavaScript -Dashboard functionality can be extended by modifying `static/admin/js/admin.js`. - -### Backend -Additional API endpoints can be added to `admin_server.go`. - -## Production Considerations - -### Security -- Add authentication/authorization -- Use HTTPS in production -- Implement rate limiting -- Add CORS configuration - -### Performance -- Enable caching for static assets -- Use compression middleware -- Monitor memory usage -- Implement connection pooling - -### Monitoring -- Add logging for admin operations -- Implement audit trails -- Monitor admin API usage -- Set up alerting for critical events - -## Troubleshooting - -### Common Issues - -1. **Dashboard not loading** - - Check if static files are in the correct location - - Verify server is running on correct port - - Check browser console for errors - -2. **API endpoints returning 404** - - Ensure admin server is started - - Verify correct port configuration - - Check route registration - -3. **Real-time updates not working** - - Check WebSocket connection in browser dev tools - - Verify broker is running and accessible - - Check for CORS issues - -### Debug Mode - -Enable debug logging: - -```go -lg := logger.NewDefaultLogger() -lg.Debug("Admin server starting") -``` - -## License - -This admin dashboard is part of the MQ package and follows the same license terms. - -## Contributing - -To contribute to the admin dashboard: - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Add tests if applicable -5. Submit a pull request - -For issues or feature requests, please open an issue on the main repository. diff --git a/examples/minimal_admin/static/admin/css/admin.css b/examples/minimal_admin/static/admin/css/admin.css deleted file mode 100644 index 4565694..0000000 --- a/examples/minimal_admin/static/admin/css/admin.css +++ /dev/null @@ -1,550 +0,0 @@ -/* Admin Dashboard Custom Styles */ - -:root { - --primary-color: #1e40af; - --secondary-color: #3b82f6; - --success-color: #10b981; - --warning-color: #f59e0b; - --danger-color: #ef4444; - --info-color: #06b6d4; - --dark-color: #374151; - --light-color: #f9fafb; -} - -/* Chart containers to prevent infinite resize loops */ -.chart-container { - position: relative; - width: 100%; - max-width: 100%; - overflow: hidden; -} - -.chart-container canvas { - display: block; - width: 100% !important; - height: auto !important; - max-width: 100%; -} - -/* Custom scrollbar */ -.scrollbar { - scrollbar-width: thin; - scrollbar-color: #d1d5db #e5e7eb; -} - -.scrollbar::-webkit-scrollbar { - width: 8px; -} - -.scrollbar::-webkit-scrollbar-track { - background: #e5e7eb; -} - -.scrollbar::-webkit-scrollbar-thumb { - background-color: #d1d5db; - border-radius: 9999px; -} - -/* Tab styles */ -.tab-btn { - border-bottom-color: transparent; - color: #6b7280; - transition: all 0.2s ease; -} - -.tab-btn.active { - border-bottom-color: var(--primary-color); - color: var(--primary-color); -} - -.tab-btn:hover:not(.active) { - color: #374151; -} - -.tab-content { - display: none; - animation: fadeIn 0.3s ease-in; -} - -.tab-content.active { - display: block; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Status indicators */ -.status-indicator { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 500; -} - -.status-running { - background-color: #d1fae5; - color: #065f46; -} - -.status-paused { - background-color: #fef3c7; - color: #92400e; -} - -.status-stopped { - background-color: #fee2e2; - color: #991b1b; -} - -.status-healthy { - background-color: #d1fae5; - color: #065f46; -} - -.status-warning { - background-color: #fef3c7; - color: #92400e; -} - -.status-error { - background-color: #fee2e2; - color: #991b1b; -} - -/* Connection status */ -.connection-status .connected { - background-color: var(--success-color); -} - -.connection-status .disconnected { - background-color: var(--danger-color); -} - -/* Card hover effects */ -.card { - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); -} - -/* Button styles */ -.btn-primary { - background-color: var(--primary-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.btn-primary:hover { - background-color: #1d4ed8; -} - -.btn-secondary { - background-color: #6b7280; - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.btn-secondary:hover { - background-color: #4b5563; -} - -.btn-success { - background-color: var(--success-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.btn-success:hover { - background-color: #059669; -} - -.btn-warning { - background-color: var(--warning-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.btn-warning:hover { - background-color: #d97706; -} - -.btn-danger { - background-color: var(--danger-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.btn-danger:hover { - background-color: #dc2626; -} - -/* Table styles */ -.table-container { - max-height: 500px; - overflow-y: auto; -} - -.table-row:hover { - background-color: #f9fafb; -} - -/* Modal styles */ -.modal { - z-index: 1000; -} - -.modal-backdrop { - background-color: rgba(0, 0, 0, 0.5); -} - -.modal-content { - animation: modalSlideIn 0.3s ease; -} - -@keyframes modalSlideIn { - from { - opacity: 0; - transform: scale(0.9) translateY(-50px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -/* Chart containers */ -.chart-container { - position: relative; - height: 300px; -} - -/* Activity feed */ -.activity-item { - display: flex; - align-items: center; - padding: 1rem 0; - border-bottom: 1px solid #e5e7eb; -} - -.activity-item:last-child { - border-bottom: none; -} - -.activity-icon { - width: 2rem; - height: 2rem; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; -} - -.activity-icon.success { - background-color: var(--success-color); - color: white; -} - -.activity-icon.warning { - background-color: var(--warning-color); - color: white; -} - -.activity-icon.error { - background-color: var(--danger-color); - color: white; -} - -.activity-icon.info { - background-color: var(--info-color); - color: white; -} - -/* Metrics cards */ -.metric-card { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-radius: 0.5rem; - padding: 1.5rem; - text-align: center; -} - -.metric-value { - font-size: 2rem; - font-weight: bold; - margin-bottom: 0.5rem; -} - -.metric-label { - font-size: 0.875rem; - opacity: 0.8; -} - -/* Health check styles */ -.health-check { - display: flex; - align-items: center; - justify-content: between; - padding: 1rem; - border-radius: 0.375rem; - margin-bottom: 0.5rem; -} - -.health-check.healthy { - background-color: #d1fae5; - border: 1px solid #10b981; -} - -.health-check.warning { - background-color: #fef3c7; - border: 1px solid #f59e0b; -} - -.health-check.error { - background-color: #fee2e2; - border: 1px solid #ef4444; -} - -.health-check-icon { - width: 1.5rem; - height: 1.5rem; - margin-right: 0.75rem; -} - -/* Loading spinner */ -.spinner { - border: 2px solid #f3f3f3; - border-top: 2px solid var(--primary-color); - border-radius: 50%; - width: 1rem; - height: 1rem; - animation: spin 1s linear infinite; - display: inline-block; - margin-left: 0.5rem; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .grid-cols-4 { - grid-template-columns: repeat(2, 1fr); - } - - .grid-cols-2 { - grid-template-columns: 1fr; - } - - .tab-btn { - padding: 0.5rem 0.25rem; - font-size: 0.875rem; - } - - .modal-content { - margin: 1rem; - max-width: calc(100% - 2rem); - } -} - -/* Toast notifications */ -.toast { - position: fixed; - top: 1rem; - right: 1rem; - background-color: white; - border-radius: 0.5rem; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); - padding: 1rem; - z-index: 2000; - max-width: 300px; - transform: translateX(400px); - transition: transform 0.3s ease; -} - -.toast.show { - transform: translateX(0); -} - -.toast.success { - border-left: 4px solid var(--success-color); -} - -.toast.warning { - border-left: 4px solid var(--warning-color); -} - -.toast.error { - border-left: 4px solid var(--danger-color); -} - -.toast.info { - border-left: 4px solid var(--info-color); -} - -/* Progress bars */ -.progress-bar { - width: 100%; - height: 0.5rem; - background-color: #e5e7eb; - border-radius: 9999px; - overflow: hidden; -} - -.progress-fill { - height: 100%; - background-color: var(--primary-color); - transition: width 0.3s ease; -} - -.progress-fill.success { - background-color: var(--success-color); -} - -.progress-fill.warning { - background-color: var(--warning-color); -} - -.progress-fill.danger { - background-color: var(--danger-color); -} - -/* Form styles */ -.form-group { - margin-bottom: 1rem; -} - -.form-label { - display: block; - margin-bottom: 0.25rem; - font-weight: 500; - color: #374151; -} - -.form-input { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - font-size: 0.875rem; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.form-input:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.form-input:invalid { - border-color: var(--danger-color); -} - -/* Custom toggle switch */ -.toggle { - position: relative; - display: inline-block; - width: 3rem; - height: 1.5rem; -} - -.toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - transition: 0.4s; - border-radius: 1.5rem; -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 1.25rem; - width: 1.25rem; - left: 0.125rem; - bottom: 0.125rem; - background-color: white; - transition: 0.4s; - border-radius: 50%; -} - -.toggle input:checked + .toggle-slider { - background-color: var(--primary-color); -} - -.toggle input:checked + .toggle-slider:before { - transform: translateX(1.5rem); -} - -/* Tooltips */ -.tooltip { - position: relative; - display: inline-block; -} - -.tooltip .tooltip-text { - visibility: hidden; - width: 120px; - background-color: #374151; - color: white; - text-align: center; - border-radius: 0.375rem; - padding: 0.5rem; - font-size: 0.75rem; - position: absolute; - z-index: 1; - bottom: 125%; - left: 50%; - margin-left: -60px; - opacity: 0; - transition: opacity 0.3s; -} - -.tooltip:hover .tooltip-text { - visibility: visible; - opacity: 1; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - :root { - --background-color: #1f2937; - --surface-color: #374151; - --text-primary: #f9fafb; - --text-secondary: #d1d5db; - } -} diff --git a/examples/minimal_admin/static/admin/index.html b/examples/minimal_admin/static/admin/index.html deleted file mode 100644 index edaca18..0000000 --- a/examples/minimal_admin/static/admin/index.html +++ /dev/null @@ -1,628 +0,0 @@ - - - - - - - MQ Admin Dashboard - - - - - - - - - - - -
- -
- -
- - -
- -
- -
-
-
-
-
-
- - - - - -
-
-
-
-
Total Messages
-
0
-
-
-
-
-
- -
-
-
-
-
- - - - -
-
-
-
-
Active Consumers
-
0
-
-
-
-
-
- -
-
-
-
-
- - - - -
-
-
-
-
Active Queues
-
0
-
-
-
-
-
- -
-
-
-
-
- - - -
-
-
-
-
Failed Messages
-
0
-
-
-
-
-
-
- - -
-
-

Message Throughput

-
- -
-
-
-

Queue Depths

-
- -
-
-
- - -
-
-

Recent Activity

-
-
    - -
-
-
-
-
- - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/examples/minimal_admin/static/admin/js/admin.js b/examples/minimal_admin/static/admin/js/admin.js deleted file mode 100644 index 6fdb104..0000000 --- a/examples/minimal_admin/static/admin/js/admin.js +++ /dev/null @@ -1,1203 +0,0 @@ -// MQ Admin Dashboard JavaScript -class MQAdminDashboard { - constructor() { - this.wsConnection = null; - this.isConnected = false; - this.charts = {}; - this.refreshInterval = null; - this.currentTab = 'overview'; - this.data = { - metrics: {}, - queues: [], - consumers: [], - pools: [], - broker: {}, - healthChecks: [] - }; - - this.init(); - } - - init() { - this.setupEventListeners(); - this.initializeCharts(); - this.connectWebSocket(); - this.startRefreshInterval(); - this.loadInitialData(); - } - - setupEventListeners() { - // Tab navigation - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - this.switchTab(e.target.dataset.tab); - }); - }); - - // Refresh button - document.getElementById('refreshBtn').addEventListener('click', () => { - this.refreshData(); - }); - - // Modal handlers - this.setupModalHandlers(); - - // Broker controls - this.setupBrokerControls(); - - // Form handlers - this.setupFormHandlers(); - } - - setupModalHandlers() { - // Consumer modal - const consumerModal = document.getElementById('consumerModal'); - const cancelConsumerBtn = document.getElementById('cancelConsumerConfig'); - - cancelConsumerBtn.addEventListener('click', () => { - consumerModal.classList.add('hidden'); - }); - - // Pool modal - const poolModal = document.getElementById('poolModal'); - const cancelPoolBtn = document.getElementById('cancelPoolConfig'); - - cancelPoolBtn.addEventListener('click', () => { - poolModal.classList.add('hidden'); - }); - - // Close modals on backdrop click - [consumerModal, poolModal].forEach(modal => { - modal.addEventListener('click', (e) => { - if (e.target === modal) { - modal.classList.add('hidden'); - } - }); - }); - } - - setupBrokerControls() { - document.getElementById('restartBroker').addEventListener('click', () => { - this.confirmAction('restart broker', () => this.restartBroker()); - }); - - document.getElementById('stopBroker').addEventListener('click', () => { - this.confirmAction('stop broker', () => this.stopBroker()); - }); - - document.getElementById('flushQueues').addEventListener('click', () => { - this.confirmAction('flush all queues', () => this.flushQueues()); - }); - - // Tasks refresh button - document.getElementById('refreshTasks').addEventListener('click', () => { - this.fetchTasks(); - }); - } - - setupFormHandlers() { - // Consumer form - document.getElementById('consumerForm').addEventListener('submit', (e) => { - e.preventDefault(); - this.updateConsumerConfig(); - }); - - // Pool form - document.getElementById('poolForm').addEventListener('submit', (e) => { - e.preventDefault(); - this.updatePoolConfig(); - }); - } - - switchTab(tabName) { - // Update active tab button - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.remove('active'); - }); - document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); - - // Show corresponding content - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - content.classList.add('hidden'); - }); - document.getElementById(tabName).classList.remove('hidden'); - document.getElementById(tabName).classList.add('active'); - - this.currentTab = tabName; - this.loadTabData(tabName); - } - - loadTabData(tabName) { - switch (tabName) { - case 'overview': - this.loadOverviewData(); - break; - case 'broker': - this.loadBrokerData(); - break; - case 'queues': - this.loadQueuesData(); - break; - case 'consumers': - this.loadConsumersData(); - break; - case 'pools': - this.loadPoolsData(); - break; - case 'tasks': - this.loadTasksData(); - break; - case 'monitoring': - this.loadMonitoringData(); - break; - } - } - - connectWebSocket() { - try { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - // Use the Socket class instead of raw WebSocket - this.wsConnection = new Socket(wsUrl, 'admin-user'); - this.wsConnection.connect().then(connected => { - console.log('WebSocket connected'); - this.wsConnection.onConnect(() => { - this.updateConnectionStatus(true); - this.showToast('Connected to MQ Admin', 'success'); - console.log('WebSocket connected'); - }); - - this.wsConnection.on('update', (data) => { - try { - // Data is already parsed by the Socket class - this.handleWebSocketMessage(data); - } catch (error) { - console.error('Failed to handle WebSocket message:', error); - } - }); - - this.wsConnection.onDisconnect(() => { - this.updateConnectionStatus(false); - this.showToast('Disconnected from MQ Admin', 'warning'); - console.log('WebSocket disconnected'); - // Socket class handles reconnection automatically - }); - }); - } catch (error) { - console.error('Failed to connect WebSocket:', error); - this.updateConnectionStatus(false); - } - } - - handleWebSocketMessage(data) { - console.log('WebSocket message received:', data); - - switch (data.type) { - case 'metrics': - this.updateMetrics(data.data); - break; - case 'queues': - this.updateQueues(data.data); - break; - case 'consumers': - this.updateConsumers(data.data); - break; - case 'pools': - this.updatePools(data.data); - break; - case 'broker': - this.updateBroker(data.data); - break; - case 'task_update': - this.handleTaskUpdate(data.data); - break; - case 'broker_restart': - case 'broker_stop': - case 'broker_pause': - case 'broker_resume': - this.showToast(data.data.message || 'Broker operation completed', 'info'); - // Refresh broker info - this.fetchBrokerInfo(); - break; - case 'consumer_pause': - case 'consumer_resume': - case 'consumer_stop': - this.showToast(data.data.message || 'Consumer operation completed', 'info'); - // Refresh consumer info - this.fetchConsumers(); - break; - case 'pool_pause': - case 'pool_resume': - case 'pool_stop': - this.showToast(data.data.message || 'Pool operation completed', 'info'); - // Refresh pool info - this.fetchPools(); - break; - case 'queue_purge': - case 'queues_flush': - this.showToast(data.data.message || 'Queue operation completed', 'info'); - // Refresh queue info - this.fetchQueues(); - break; - default: - console.log('Unknown WebSocket message type:', data.type); - } - } - - handleTaskUpdate(taskData) { - // Add to activity feed - const activity = { - type: 'task', - message: `Task ${taskData.task_id} ${taskData.status} in queue ${taskData.queue}`, - timestamp: new Date(taskData.updated_at), - status: taskData.status === 'completed' ? 'success' : taskData.status === 'failed' ? 'error' : 'info' - }; - this.addActivity(activity); - - // Update metrics if on overview tab - if (this.currentTab === 'overview') { - this.fetchMetrics(); - } - } - - updateConnectionStatus(connected) { - this.isConnected = connected; - const indicator = document.getElementById('connectionIndicator'); - const status = document.getElementById('connectionStatus'); - - if (connected) { - indicator.className = 'w-3 h-3 bg-green-500 rounded-full'; - status.textContent = 'Connected'; - } else { - indicator.className = 'w-3 h-3 bg-red-500 rounded-full'; - status.textContent = 'Disconnected'; - } - } - - initializeCharts() { - // Throughput Chart - const throughputCtx = document.getElementById('throughputChart').getContext('2d'); - this.charts.throughput = new Chart(throughputCtx, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: 'Messages/sec', - data: [], - borderColor: 'rgb(59, 130, 246)', - backgroundColor: 'rgba(59, 130, 246, 0.1)', - tension: 0.4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - animation: false, // Disable animations to prevent loops - scales: { - y: { - beginAtZero: true - } - }, - plugins: { - legend: { - display: false - } - } - } - }); - - // Queue Depth Chart - const queueDepthCtx = document.getElementById('queueDepthChart').getContext('2d'); - this.charts.queueDepth = new Chart(queueDepthCtx, { - type: 'bar', - data: { - labels: [], - datasets: [{ - label: 'Queue Depth', - data: [], - backgroundColor: 'rgba(16, 185, 129, 0.8)', - borderColor: 'rgb(16, 185, 129)', - borderWidth: 1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true - } - }, - plugins: { - legend: { - display: false - } - } - } - }); - - // System Performance Chart - const systemCtx = document.getElementById('systemChart').getContext('2d'); - this.charts.system = new Chart(systemCtx, { - type: 'line', - data: { - labels: [], - datasets: [ - { - label: 'CPU %', - data: [], - borderColor: 'rgb(239, 68, 68)', - backgroundColor: 'rgba(239, 68, 68, 0.1)', - tension: 0.4 - }, - { - label: 'Memory %', - data: [], - borderColor: 'rgb(245, 158, 11)', - backgroundColor: 'rgba(245, 158, 11, 0.1)', - tension: 0.4 - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - max: 100 - } - } - } - }); - - // Error Rate Chart - const errorCtx = document.getElementById('errorChart').getContext('2d'); - this.charts.error = new Chart(errorCtx, { - type: 'doughnut', - data: { - labels: ['Success', 'Failed'], - datasets: [{ - data: [0, 0], - backgroundColor: [ - 'rgba(16, 185, 129, 0.8)', - 'rgba(239, 68, 68, 0.8)' - ], - borderWidth: 0 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom' - } - } - } - }); - } - - async loadInitialData() { - try { - await Promise.all([ - this.fetchMetrics(), - this.fetchQueues(), - this.fetchConsumers(), - this.fetchPools(), - this.fetchBrokerInfo(), - this.fetchHealthChecks() - ]); - } catch (error) { - console.error('Failed to load initial data:', error); - this.showToast('Failed to load initial data', 'error'); - } - } - - async fetchMetrics() { - try { - const response = await fetch('/api/admin/metrics'); - const metrics = await response.json(); - this.updateMetrics(metrics); - } catch (error) { - console.error('Failed to fetch metrics:', error); - } - } - - async fetchQueues() { - try { - const response = await fetch('/api/admin/queues'); - const queues = await response.json(); - this.updateQueues(queues); - } catch (error) { - console.error('Failed to fetch queues:', error); - } - } - - async fetchConsumers() { - try { - const response = await fetch('/api/admin/consumers'); - const consumers = await response.json(); - this.updateConsumers(consumers); - } catch (error) { - console.error('Failed to fetch consumers:', error); - } - } - - async fetchPools() { - try { - const response = await fetch('/api/admin/pools'); - const pools = await response.json(); - this.updatePools(pools); - } catch (error) { - console.error('Failed to fetch pools:', error); - } - } - - async fetchBrokerInfo() { - try { - const response = await fetch('/api/admin/broker'); - const broker = await response.json(); - this.updateBroker(broker); - } catch (error) { - console.error('Failed to fetch broker info:', error); - } - } - - async fetchHealthChecks() { - try { - const response = await fetch('/api/admin/health'); - const healthChecks = await response.json(); - this.updateHealthChecks(healthChecks); - } catch (error) { - console.error('Failed to fetch health checks:', error); - } - } - - async fetchTasks() { - try { - const response = await fetch('/api/admin/tasks'); - const data = await response.json(); - this.updateTasks(data.tasks || []); - } catch (error) { - console.error('Failed to fetch tasks:', error); - } - } - - updateTasks(tasks) { - this.data.tasks = tasks; - - // Update task count cards - const activeTasks = tasks.filter(task => task.status === 'queued' || task.status === 'processing').length; - const completedTasks = tasks.filter(task => task.status === 'completed').length; - const failedTasks = tasks.filter(task => task.status === 'failed').length; - const queuedTasks = tasks.filter(task => task.status === 'queued').length; - - document.getElementById('activeTasks').textContent = activeTasks; - document.getElementById('completedTasks').textContent = completedTasks; - document.getElementById('failedTasks').textContent = failedTasks; - document.getElementById('queuedTasks').textContent = queuedTasks; - - // Update tasks table - this.renderTasksTable(tasks); - } - - renderTasksTable(tasks) { - const tbody = document.getElementById('tasksTableBody'); - tbody.innerHTML = ''; - - if (!tasks || tasks.length === 0) { - tbody.innerHTML = ` - - - No tasks found - - - `; - return; - } - - tasks.forEach(task => { - const row = document.createElement('tr'); - row.className = 'hover:bg-gray-50'; - - const statusClass = this.getStatusClass(task.status); - const createdAt = this.formatTime(task.created_at); - const payload = typeof task.payload === 'string' ? task.payload : JSON.stringify(task.payload); - const truncatedPayload = payload.length > 50 ? payload.substring(0, 50) + '...' : payload; - - row.innerHTML = ` - - ${task.id} - - - ${task.queue} - - - - ${task.status} - - - - ${task.retry_count || 0} - - - ${createdAt} - - - ${truncatedPayload} - - `; - - tbody.appendChild(row); - }); - } - - updateMetrics(metrics) { - this.data.metrics = metrics; - - // Update overview cards - document.getElementById('totalMessages').textContent = this.formatNumber(metrics.total_messages || 0); - document.getElementById('activeConsumers').textContent = metrics.active_consumers || 0; - document.getElementById('activeQueues').textContent = metrics.active_queues || 0; - document.getElementById('failedMessages').textContent = this.formatNumber(metrics.failed_messages || 0); - - // Update charts - this.updateThroughputChart(metrics.throughput_history || []); - this.updateErrorChart(metrics.success_count || 0, metrics.error_count || 0); - } - - updateQueues(queues) { - this.data.queues = queues; - this.renderQueuesTable(queues); - this.updateQueueDepthChart(queues); - } - - updateConsumers(consumers) { - this.data.consumers = consumers; - this.renderConsumersTable(consumers); - } - - updatePools(pools) { - this.data.pools = pools; - this.renderPoolsTable(pools); - } - - updateBroker(broker) { - this.data.broker = broker; - this.renderBrokerInfo(broker); - } - - updateHealthChecks(healthChecks) { - this.data.healthChecks = healthChecks; - this.renderHealthChecks(healthChecks); - } - - updateThroughputChart(throughputHistory) { - const chart = this.charts.throughput; - if (!chart || !throughputHistory) return; - - try { - const now = new Date(); - - // Keep last 20 data points - chart.data.labels = throughputHistory.map((_, index) => { - const time = new Date(now.getTime() - (throughputHistory.length - index - 1) * 5000); - return time.toLocaleTimeString(); - }); - - chart.data.datasets[0].data = throughputHistory; - chart.update('none'); - } catch (error) { - console.error('Error updating throughput chart:', error); - } - } - - updateQueueDepthChart(queues) { - const chart = this.charts.queueDepth; - if (!chart || !queues) return; - - try { - chart.data.labels = queues.map(q => q.name); - chart.data.datasets[0].data = queues.map(q => q.depth || 0); - chart.update('none'); - } catch (error) { - console.error('Error updating queue depth chart:', error); - } - } - - updateErrorChart(successCount, errorCount) { - const chart = this.charts.error; - if (!chart) return; - - try { - chart.data.datasets[0].data = [successCount, errorCount]; - chart.update('none'); - } catch (error) { - console.error('Error updating error chart:', error); - } - } - - renderQueuesTable(queues) { - const tbody = document.getElementById('queuesTable'); - tbody.innerHTML = ''; - - queues.forEach(queue => { - const row = document.createElement('tr'); - row.className = 'table-row'; - row.innerHTML = ` - ${queue.name} - ${queue.depth || 0} - ${queue.consumers || 0} - ${queue.rate || 0}/sec - - - - - `; - tbody.appendChild(row); - }); - } - - renderConsumersTable(consumers) { - const tbody = document.getElementById('consumersTable'); - tbody.innerHTML = ''; - - consumers.forEach(consumer => { - const row = document.createElement('tr'); - row.className = 'table-row'; - row.innerHTML = ` - ${consumer.id} - ${consumer.queue} - - ${consumer.status} - - ${consumer.processed || 0} - ${consumer.errors || 0} - - - - - - `; - tbody.appendChild(row); - }); - } - - renderPoolsTable(pools) { - const tbody = document.getElementById('poolsTable'); - tbody.innerHTML = ''; - - pools.forEach(pool => { - const row = document.createElement('tr'); - row.className = 'table-row'; - row.innerHTML = ` - ${pool.id} - ${pool.workers || 0} - ${pool.queue_size || 0} - ${pool.active_tasks || 0} - - ${pool.status} - - - - - - - `; - tbody.appendChild(row); - }); - } - - renderBrokerInfo(broker) { - document.getElementById('brokerStatus').textContent = broker.status || 'Unknown'; - document.getElementById('brokerStatus').className = `px-2 py-1 text-xs rounded-full ${this.getStatusClass(broker.status)}`; - document.getElementById('brokerAddress').textContent = broker.address || 'N/A'; - document.getElementById('brokerUptime').textContent = this.formatDuration(broker.uptime || 0); - document.getElementById('brokerConnections').textContent = broker.connections || 0; - - // Render broker configuration - this.renderBrokerConfig(broker.config || {}); - } - - renderBrokerConfig(config) { - const container = document.getElementById('brokerConfig'); - container.innerHTML = ''; - - Object.entries(config).forEach(([key, value]) => { - const div = document.createElement('div'); - div.className = 'flex justify-between items-center p-3 bg-gray-50 rounded'; - div.innerHTML = ` - ${this.formatConfigKey(key)}: - ${this.formatConfigValue(value)} - `; - container.appendChild(div); - }); - } - - renderHealthChecks(healthChecks) { - const container = document.getElementById('healthChecks'); - container.innerHTML = ''; - - healthChecks.forEach(check => { - const div = document.createElement('div'); - div.className = `health-check ${check.status}`; - div.innerHTML = ` -
-
-
- ${this.getHealthIcon(check.status)} -
-
-
${check.name}
-
${check.message}
-
-
-
- ${this.formatDuration(check.duration)} -
-
- `; - container.appendChild(div); - }); - } - - addActivity(activity) { - const feed = document.getElementById('activityFeed'); - const item = document.createElement('li'); - item.className = 'activity-item'; - - item.innerHTML = ` -
- ${this.getActivityIcon(activity.type)} -
-
-
${activity.title}
-
${activity.description}
-
${this.formatTime(activity.timestamp)}
-
- `; - - feed.insertBefore(item, feed.firstChild); - - // Keep only last 50 items - while (feed.children.length > 50) { - feed.removeChild(feed.lastChild); - } - } - - // Action methods - async pauseConsumer(consumerId) { - try { - const consumer = this.data.consumers.find(c => c.id === consumerId); - const action = consumer?.status === 'paused' ? 'resume' : 'pause'; - - const response = await fetch(`/api/admin/consumers/${action}?id=${consumerId}`, { - method: 'POST' - }); - - if (response.ok) { - this.showToast(`Consumer ${action}d successfully`, 'success'); - this.fetchConsumers(); - } else { - throw new Error(`Failed to ${action} consumer`); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async stopConsumer(consumerId) { - this.confirmAction('stop this consumer', async () => { - try { - const response = await fetch(`/api/admin/consumers/stop?id=${consumerId}`, { - method: 'POST' - }); - - if (response.ok) { - this.showToast('Consumer stopped successfully', 'success'); - this.fetchConsumers(); - } else { - throw new Error('Failed to stop consumer'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - }); - } - - configureConsumer(consumerId) { - const consumer = this.data.consumers.find(c => c.id === consumerId); - if (!consumer) return; - - document.getElementById('consumerIdField').value = consumer.id; - document.getElementById('maxConcurrentTasks').value = consumer.max_concurrent_tasks || 10; - document.getElementById('taskTimeout').value = consumer.task_timeout || 30; - document.getElementById('maxRetries').value = consumer.max_retries || 3; - - document.getElementById('consumerModal').classList.remove('hidden'); - } - - async updateConsumerConfig() { - try { - const consumerId = document.getElementById('consumerIdField').value; - const config = { - max_concurrent_tasks: parseInt(document.getElementById('maxConcurrentTasks').value), - task_timeout: parseInt(document.getElementById('taskTimeout').value), - max_retries: parseInt(document.getElementById('maxRetries').value) - }; - - const response = await fetch(`/api/admin/consumers/${consumerId}/config`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - if (response.ok) { - this.showToast('Consumer configuration updated', 'success'); - document.getElementById('consumerModal').classList.add('hidden'); - this.fetchConsumers(); - } else { - throw new Error('Failed to update consumer configuration'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async pausePool(poolId) { - try { - const pool = this.data.pools.find(p => p.id === poolId); - const action = pool?.status === 'paused' ? 'resume' : 'pause'; - - const response = await fetch(`/api/admin/pools/${poolId}/${action}`, { - method: 'POST' - }); - - if (response.ok) { - this.showToast(`Pool ${action}d successfully`, 'success'); - this.fetchPools(); - } else { - throw new Error(`Failed to ${action} pool`); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async stopPool(poolId) { - this.confirmAction('stop this pool', async () => { - try { - const response = await fetch(`/api/admin/pools/${poolId}/stop`, { - method: 'POST' - }); - - if (response.ok) { - this.showToast('Pool stopped successfully', 'success'); - this.fetchPools(); - } else { - throw new Error('Failed to stop pool'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - }); - } - - configurePool(poolId) { - const pool = this.data.pools.find(p => p.id === poolId); - if (!pool) return; - - document.getElementById('poolIdField').value = pool.id; - document.getElementById('numWorkers').value = pool.workers || 4; - document.getElementById('queueSize').value = pool.queue_size || 100; - document.getElementById('maxMemoryLoad').value = pool.max_memory_load || 5000000; - - document.getElementById('poolModal').classList.remove('hidden'); - } - - async updatePoolConfig() { - try { - const poolId = document.getElementById('poolIdField').value; - const config = { - workers: parseInt(document.getElementById('numWorkers').value), - queue_size: parseInt(document.getElementById('queueSize').value), - max_memory_load: parseInt(document.getElementById('maxMemoryLoad').value) - }; - - const response = await fetch(`/api/admin/pools/${poolId}/config`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - if (response.ok) { - this.showToast('Pool configuration updated', 'success'); - document.getElementById('poolModal').classList.add('hidden'); - this.fetchPools(); - } else { - throw new Error('Failed to update pool configuration'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async purgeQueue(queueName) { - this.confirmAction(`purge queue "${queueName}"`, async () => { - try { - const response = await fetch(`/api/admin/queues/${queueName}/purge`, { - method: 'POST' - }); - - if (response.ok) { - this.showToast('Queue purged successfully', 'success'); - this.fetchQueues(); - } else { - throw new Error('Failed to purge queue'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - }); - } - - async restartBroker() { - try { - const response = await fetch('/api/admin/broker/restart', { - method: 'POST' - }); - - if (response.ok) { - this.showToast('Broker restart initiated', 'success'); - this.fetchBrokerInfo(); - } else { - throw new Error('Failed to restart broker'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async stopBroker() { - try { - const response = await fetch('/api/admin/broker/stop', { - method: 'POST' - }); - - if (response.ok) { - this.showToast('Broker stop initiated', 'warning'); - this.fetchBrokerInfo(); - } else { - throw new Error('Failed to stop broker'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - async flushQueues() { - try { - const response = await fetch('/api/admin/queues/flush', { - method: 'POST' - }); - - if (response.ok) { - this.showToast('All queues flushed', 'success'); - this.fetchQueues(); - } else { - throw new Error('Failed to flush queues'); - } - } catch (error) { - this.showToast(error.message, 'error'); - } - } - - // Utility methods - confirmAction(action, callback) { - if (confirm(`Are you sure you want to ${action}?`)) { - callback(); - } - } - - showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - toast.innerHTML = ` -
- ${message} - -
- `; - - document.body.appendChild(toast); - - // Show toast - setTimeout(() => toast.classList.add('show'), 100); - - // Auto-remove after 5 seconds - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 5000); - } - - formatNumber(num) { - if (num >= 1000000) { - return (num / 1000000).toFixed(1) + 'M'; - } else if (num >= 1000) { - return (num / 1000).toFixed(1) + 'K'; - } - return num.toString(); - } - - formatDuration(ms) { - if (ms < 1000) return `${ms}ms`; - if (ms < 60000) return `${Math.floor(ms / 1000)}s`; - if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; - const hours = Math.floor(ms / 3600000); - const minutes = Math.floor((ms % 3600000) / 60000); - return `${hours}h ${minutes}m`; - } - - formatTime(timestamp) { - return new Date(timestamp).toLocaleTimeString(); - } - - formatConfigKey(key) { - return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - } - - formatConfigValue(value) { - if (typeof value === 'boolean') return value ? 'Yes' : 'No'; - if (typeof value === 'object') return JSON.stringify(value); - return value.toString(); - } - - getStatusClass(status) { - switch (status?.toLowerCase()) { - case 'running': - case 'active': - case 'healthy': - return 'status-running'; - case 'paused': - case 'warning': - return 'status-paused'; - case 'stopped': - case 'error': - case 'failed': - return 'status-stopped'; - default: - return 'status-paused'; - } - } - - getHealthIcon(status) { - switch (status) { - case 'healthy': - return '✓'; - case 'warning': - return '⚠'; - case 'error': - return '✗'; - default: - return '?'; - } - } - - getActivityIcon(type) { - switch (type) { - case 'success': - return '✓'; - case 'warning': - return '⚠'; - case 'error': - return '✗'; - case 'info': - default: - return 'i'; - } - } - - refreshData() { - this.loadInitialData(); - this.showToast('Data refreshed', 'success'); - } - - startRefreshInterval() { - // Clear any existing interval first - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - - // Refresh data every 15 seconds (reduced frequency to prevent overload) - this.refreshInterval = setInterval(() => { - if (this.isConnected && this.currentTab) { - try { - this.loadTabData(this.currentTab); - } catch (error) { - console.error('Error during refresh:', error); - } - } - }, 15000); - } - - loadOverviewData() { - this.fetchMetrics(); - } - - loadBrokerData() { - this.fetchBrokerInfo(); - } - - loadQueuesData() { - this.fetchQueues(); - } - - loadConsumersData() { - this.fetchConsumers(); - } - - loadPoolsData() { - this.fetchPools(); - } - - loadTasksData() { - this.fetchTasks(); - } - - loadMonitoringData() { - this.fetchHealthChecks(); - this.fetchMetrics(); - } -} - -// Initialize dashboard when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - window.adminDashboard = new MQAdminDashboard(); -}); diff --git a/examples/minimal_admin/static/admin/js/socket.js b/examples/minimal_admin/static/admin/js/socket.js deleted file mode 100644 index ceb8686..0000000 --- a/examples/minimal_admin/static/admin/js/socket.js +++ /dev/null @@ -1,249 +0,0 @@ -class Socket { - events = {} - reconnectInterval - reconnectOpts = { enabled: true, replayOnConnect: true, intervalMS: 5000 } - reconnecting = false - connectedOnce = false - headerStartCharCode = 1 - headerStartChar - dataStartCharCode = 2 - dataStartChar - subProtocol = 'sac-sock' - ws - reconnected = false - maxAttempts = 3 - totalAttempts = 0 - kickedOut = false - userID - url - - constructor(url, userID, opts = { reconnectOpts: {} }) { - opts = opts || { reconnectOpts: {} }; - this.headerStartChar = String.fromCharCode(this.headerStartCharCode) - this.dataStartChar = String.fromCharCode(this.dataStartCharCode) - this.url = url - this.userID = userID - if (typeof opts.reconnectOpts == 'object') { - for (let i in opts.reconnectOpts) { - if (!opts.reconnectOpts.hasOwnProperty(i)) continue; - this.reconnectOpts[i] = opts.reconnectOpts[i]; - } - } - } - - noop() { } - - async connect(timeout = 10000) { - try { - this.ws = new WebSocket(this.url, this.subProtocol); - this.ws.binaryType = 'arraybuffer'; - this.handleEvents() - const isOpened = () => (this.ws.readyState === WebSocket.OPEN) - - if (this.ws.readyState !== WebSocket.CONNECTING) { - return isOpened() - } - } catch (err) { - console.log("Error on reconnection", err) - } - - } - - handleEvents() { - let self = this - this.onConnect(this.noop) - this.onDisconnect(this.noop) - this.ws.onmessage = function (e) { - let msg = e.data, - headers = {}, - eventName = '', - data = '', - chr = null, - i, msgLen; - - if (typeof msg === 'string') { - let dataStarted = false, - headerStarted = false; - - for (i = 0, msgLen = msg.length; i < msgLen; i++) { - chr = msg[i]; - if (!dataStarted && !headerStarted && chr !== self.dataStartChar && chr !== self.headerStartChar) { - eventName += chr; - } else if (!headerStarted && chr === self.headerStartChar) { - headerStarted = true; - } else if (headerStarted && !dataStarted && chr !== self.dataStartChar) { - headers[chr] = true; - } else if (!dataStarted && chr === self.dataStartChar) { - dataStarted = true; - } else { - data += chr; - } - } - } else if (msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined) { - let dv = new DataView(msg), - headersStarted = false; - - for (i = 0, msgLen = dv.byteLength; i < msgLen; i++) { - chr = dv.getUint8(i); - - if (chr !== self.dataStartCharCode && chr !== self.headerStartCharCode && !headersStarted) { - eventName += String.fromCharCode(chr); - } else if (chr === self.headerStartCharCode && !headersStarted) { - headersStarted = true; - } else if (headersStarted && chr !== self.dataStartCharCode) { - headers[String.fromCharCode(chr)] = true; - } else if (chr === self.dataStartCharCode) { - // @ts-ignore - data = dv.buffer.slice(i + 1); - break; - } - } - } - - if (eventName.length === 0) return; //no event to dispatch - if (typeof self.events[eventName] === 'undefined') return; - // @ts-ignore - self.events[eventName].call(self, (headers.J) ? JSON.parse(data) : data); - } - } - - startReconnect(timeout = 10000) { - let self = this - setTimeout(async function () { - try { - if (self.maxAttempts > self.totalAttempts) { - return - } - let newWS = new WebSocket(self.url, self.subProtocol); - self.totalAttempts += 1 - console.log("attempt to reconnect...", self.totalAttempts) - newWS.onmessage = self.ws.onmessage; - newWS.onclose = self.ws.onclose; - newWS.binaryType = self.ws.binaryType; - self.handleEvents() - - //we need to run the initially set onConnect function on the first successful connecting, - //even if replayOnConnect is disabled. The server might not be available on a first - //connection attempt. - if (self.reconnectOpts.replayOnConnect || !self.connectedOnce) { - newWS.onopen = self.ws.onopen; - } - self.ws = newWS; - if (!self.reconnectOpts.replayOnConnect && self.connectedOnce) { - self.onConnect(self.noop); - } - self.ws = newWS - const isOpened = () => (self.ws.readyState === WebSocket.OPEN) - - if (self.ws.readyState !== WebSocket.CONNECTING) { - const opened = isOpened() - if (!opened) { - self.startReconnect(timeout) - } else { - console.log("connected with signal server") - } - return opened - } - else { - const intrasleep = 100 - const ttl = timeout / intrasleep // time to loop - let loop = 0 - while (self.ws.readyState === WebSocket.CONNECTING && loop < ttl) { - await new Promise(resolve => setTimeout(resolve, intrasleep)) - loop++ - } - const opened = isOpened() - if (!opened) { - self.startReconnect(timeout) - } else { - console.log("connected with signal server") - } - return opened - } - } catch (err) { - console.log("Error on reconnection", err) - } - }, self.reconnectOpts.intervalMS); - } - - onConnect(callback) { - let self = this - this.ws.onopen = function () { - self.connectedOnce = true; - callback.apply(self, arguments); - if (self.reconnecting) { - self.reconnecting = false; - } - }; - }; - - onDisconnect(callback) { - let self = this - this.ws.onclose = function () { - if (!self.reconnecting && self.connectedOnce) { - callback.apply(self, arguments); - } - if (self.reconnectOpts.enabled && !self.kickedOut) { - self.reconnecting = true; - self.startReconnect(); - } - }; - }; - - on(eventName, callback, override) { - override = override || false - if (!this.events.hasOwnProperty(eventName)) { - this.events[eventName] = callback; - } else if (override) { - this.off(eventName) - this.events[eventName] = callback; - } - } - off(eventName) { - if (this.events[eventName]) { - delete this.events[eventName]; - } - } - - emit(eventName, data) { - let rs = this.ws.readyState; - if (rs === 0) { - console.warn("websocket is not open yet"); - return; - } else if (rs === 2 || rs === 3) { - console.error("websocket is closed"); - return; - } - let msg; - if (data instanceof ArrayBuffer) { - let ab = new ArrayBuffer(data.byteLength + eventName.length + 1), - newBuf = new DataView(ab), - oldBuf = new DataView(data), - i = 0; - for (let evtLen = eventName.length; i < evtLen; i++) { - newBuf.setUint8(i, eventName.charCodeAt(i)); - } - newBuf.setUint8(i, this.dataStartCharCode); - i++; - for (let x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++) { - newBuf.setUint8(i, oldBuf.getUint8(x)); - } - msg = ab; - } else if (typeof data === 'object') { - msg = eventName + this.dataStartChar + JSON.stringify(data); - } else { - msg = eventName + this.dataStartChar + data; - } - this.ws.send(msg); - } - - close() { - this.reconnectOpts.enabled = false; - return this.ws.close(1000); - } -} - -// Initialize dashboard when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - window.Socket = Socket; -}); diff --git a/examples/parse.go b/examples/parse.go deleted file mode 100644 index 7298d2d..0000000 --- a/examples/parse.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/oarkflow/form" -) - -func main() { - queryString := []byte("fields[0][method]=GET&fields[0][path]=/user/:id&fields[0][handlerMsg]=User Profile&fields[1][method]=POST&fields[1][path]=/user/create&fields[1][handlerMsg]=Create User") - fmt.Println(form.DecodeForm(queryString)) -} diff --git a/examples/pool.go b/examples/pool.go deleted file mode 100644 index 33a3cec..0000000 --- a/examples/pool.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "context" - "fmt" - "math/rand" - "os/signal" - "syscall" - "time" - - "github.com/oarkflow/json" - - v1 "github.com/oarkflow/mq" -) - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - pool := v1.NewPool(5, - v1.WithTaskStorage(v1.NewMemoryTaskStorage(10*time.Minute)), - v1.WithHandler(func(ctx context.Context, payload *v1.Task) v1.Result { - v1.Logger.Info().Str("taskID", payload.ID).Msg("Processing task payload") - time.Sleep(500 * time.Millisecond) - return v1.Result{} - }), - v1.WithPoolCallback(func(ctx context.Context, result v1.Result) error { - v1.Logger.Info().Msg("Task callback invoked") - return nil - }), - v1.WithCircuitBreaker(v1.CircuitBreakerConfig{ - Enabled: true, - FailureThreshold: 3, - ResetTimeout: 5 * time.Second, - }), - v1.WithWarningThresholds(v1.ThresholdConfig{ - HighMemory: v1.Config.WarningThreshold.HighMemory, - LongExecution: v1.Config.WarningThreshold.LongExecution, - }), - v1.WithDiagnostics(true), - v1.WithMetricsRegistry(v1.NewInMemoryMetricsRegistry()), - v1.WithGracefulShutdown(10*time.Second), - v1.WithPlugin(&v1.DefaultPlugin{}), - ) - defer func() { - metrics := pool.Metrics() - v1.Logger.Info().Msgf("Metrics: %+v", metrics) - pool.Stop() - v1.Logger.Info().Msgf("Dead Letter Queue has %d tasks", len(pool.DLQ().Tasks())) - }() - - go func() { - for i := 0; i < 50; i++ { - task := &v1.Task{ - ID: "", - Payload: json.RawMessage(fmt.Sprintf("Task Payload %d", i)), - } - if err := pool.EnqueueTask(context.Background(), task, rand.Intn(10)); err != nil { - v1.Logger.Error().Err(err).Msg("Failed to enqueue task") - } - time.Sleep(200 * time.Millisecond) - } - }() - - <-ctx.Done() - v1.Logger.Info().Msg("Received shutdown signal, exiting...") -} diff --git a/examples/priority.go b/examples/priority.go deleted file mode 100644 index bb1e9bd..0000000 --- a/examples/priority.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "context" - "time" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - pool := mq.NewPool(2, - mq.WithTaskQueueSize(5), - mq.WithMaxMemoryLoad(1000), - mq.WithHandler(tasks.SchedulerHandler), - mq.WithPoolCallback(tasks.SchedulerCallback), - mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)), - mq.WithDiagnostics(false), - ) - - for i := 0; i < 100; i++ { - if i%10 == 0 { - pool.EnqueueTask(context.Background(), &mq.Task{ID: "High Priority Task: I'm high"}, 10) - } else if i%15 == 0 { - pool.EnqueueTask(context.Background(), &mq.Task{ID: "Super High Priority Task: {}"}, 15) - } else { - pool.EnqueueTask(context.Background(), &mq.Task{ID: "Low Priority Task"}, 1) - } - } - - time.Sleep(15 * time.Second) - pool.Metrics() - pool.Stop() -} diff --git a/examples/publisher.go b/examples/publisher.go deleted file mode 100644 index f823f96..0000000 --- a/examples/publisher.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/oarkflow/mq" -) - -func main() { - payload := []byte(`{"phone": "+123456789", "email": "abc.xyz@gmail.com", "age": 12}`) - task := mq.Task{ - Payload: payload, - } - publisher := mq.NewPublisher("publish-1", mq.WithBrokerURL(":8081")) - for i := 0; i < 2; i++ { - // publisher := mq.NewPublisher("publish-1", mq.WithTLS(true, "./certs/server.crt", "./certs/server.key")) - err := publisher.Publish(context.Background(), task, "queue1") - if err != nil { - panic(err) - } - } - fmt.Println("Async task published successfully") -} diff --git a/examples/scheduler.go b/examples/scheduler.go deleted file mode 100644 index 0a0e06e..0000000 --- a/examples/scheduler.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - handler := tasks.SchedulerHandler - callback := tasks.SchedulerCallback - - // Initialize the pool with various parameters. - pool := mq.NewPool(3, - mq.WithTaskQueueSize(5), - mq.WithMaxMemoryLoad(1000), - mq.WithHandler(handler), - mq.WithPoolCallback(callback), - mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)), - ) - scheduler := mq.NewScheduler(pool) - scheduler.Start() - ctx := context.Background() - - // ------------------------------- - // Special String Examples - // ------------------------------- - - // Task scheduled with @every for a duration of 30 seconds. - scheduler.AddTask(ctx, &mq.Task{ID: "Every 30 Seconds Task"}, - mq.WithScheduleSpec("@every 30s"), - mq.WithRecurring(), - ) - - // Task scheduled with @every using a 1-minute duration. - scheduler.AddTask(ctx, &mq.Task{ID: "Every Minute Task"}, - mq.WithScheduleSpec("@every 1m"), - mq.WithRecurring(), - ) - - // Task scheduled with @daily (runs at midnight by default). - scheduler.AddTask(ctx, &mq.Task{ID: "Daily Task"}, - mq.WithScheduleSpec("@daily"), - mq.WithRecurring(), - ) - - // Task scheduled with @weekly (runs at midnight on Sundays). - scheduler.AddTask(ctx, &mq.Task{ID: "Weekly Task"}, - mq.WithScheduleSpec("@weekly"), - mq.WithRecurring(), - ) - - // Task scheduled with @monthly (runs on the 1st of every month at midnight). - scheduler.AddTask(ctx, &mq.Task{ID: "Monthly Task"}, - mq.WithScheduleSpec("@monthly"), - mq.WithRecurring(), - ) - - // Task scheduled with @yearly (or @annually) – runs on January 1st at midnight. - scheduler.AddTask(ctx, &mq.Task{ID: "Yearly Task"}, - mq.WithScheduleSpec("@yearly"), - mq.WithRecurring(), - ) - - // ------------------------------- - // Cron Spec Examples - // ------------------------------- - - // Example using a standard 5-field cron expression: - // "0 * * * *" means at minute 0 of every hour. - scheduler.AddTask(ctx, &mq.Task{ID: "Cron 5-field Task"}, - mq.WithScheduleSpec("0 * * * *"), - mq.WithRecurring(), - ) - - // Example using an extended 6-field cron expression: - // "30 * * * * *" means at 30 seconds past every minute. - scheduler.AddTask(ctx, &mq.Task{ID: "Cron 6-field Task"}, - mq.WithScheduleSpec("30 * * * * *"), - mq.WithRecurring(), - ) - - // ------------------------------- - // Example Task Enqueuing (Immediate Tasks) - // ------------------------------- - // These tasks are enqueued immediately into the pool queue. - pool.EnqueueTask(context.Background(), &mq.Task{ID: "Immediate Task 1"}, 1) - time.Sleep(1 * time.Second) - pool.EnqueueTask(context.Background(), &mq.Task{ID: "Immediate Task 2"}, 5) - - time.Sleep(10 * time.Minute) - // Remove scheduled tasks after demonstration. - scheduler.RemoveTask("Every 30 Seconds Task") - scheduler.RemoveTask("Every Minute Task") - scheduler.RemoveTask("Daily Task") - scheduler.RemoveTask("Weekly Task") - scheduler.RemoveTask("Monthly Task") - scheduler.RemoveTask("Yearly Task") - scheduler.RemoveTask("Cron 5-field Task") - scheduler.RemoveTask("Cron 6-field Task") - scheduler.Close() - - // Retrieve metrics, then stop the pool. - fmt.Println(pool.FormattedMetrics()) -} diff --git a/examples/schema.json b/examples/schema.json deleted file mode 100644 index 4195ea0..0000000 --- a/examples/schema.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "type": "object", - "properties": { - "first_name": { - "type": "string", - "title": "First Name", - "order": 1, - "ui": { - "element": "input", - "class": "form-group", - "name": "first_name" - } - }, - "last_name": { - "type": "string", - "title": "Last Name", - "order": 2, - "ui": { - "element": "input", - "class": "form-group", - "name": "last_name" - } - }, - "email": { - "type": "email", - "title": "Email Address", - "order": 3, - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "name": "email" - } - }, - "user_type": { - "type": "string", - "title": "User Type", - "order": 4, - "ui": { - "element": "select", - "class": "form-group", - "name": "user_type", - "options": [ "new", "premium", "standard" ] - } - }, - "priority": { - "type": "string", - "title": "Priority Level", - "order": 5, - "ui": { - "element": "select", - "class": "form-group", - "name": "priority", - "options": [ "low", "medium", "high", "urgent" ] - } - }, - "subject": { - "type": "string", - "title": "Subject", - "order": 6, - "ui": { - "element": "input", - "class": "form-group", - "name": "subject" - } - }, - "message": { - "type": "textarea", - "title": "Message", - "order": 7, - "ui": { - "element": "textarea", - "class": "form-group", - "name": "message" - } - } - }, - "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ], - "form": { - "class": "form-horizontal", - "action": "/process?task_id={{task_id}}&next=true", - "method": "POST", - "enctype": "application/x-www-form-urlencoded", - "groups": [ - { - "title": "User Information", - "fields": [ "first_name", "last_name", "email" ] - }, - { - "title": "Ticket Details", - "fields": [ "user_type", "priority", "subject", "message" ] - } - ], - "submit": { - "type": "submit", - "label": "Submit", - "class": "btn btn-primary" - }, - "reset": { - "type": "reset", - "label": "Reset", - "class": "btn btn-secondary" - } - } -} diff --git a/examples/server.go b/examples/server.go deleted file mode 100644 index 450149f..0000000 --- a/examples/server.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "context" - - "github.com/oarkflow/mq" - - "github.com/oarkflow/mq/examples/tasks" -) - -func main() { - b := mq.NewBroker(mq.WithCallback(tasks.Callback), mq.WithBrokerURL(":8081")) - // b := mq.NewBroker(mq.WithCallback(tasks.Callback), mq.WithTLS(true, "./certs/server.crt", "./certs/server.key"), mq.WithCAPath("./certs/ca.cert")) - b.NewQueue("queue1") - b.NewQueue("queue2") - b.Start(context.Background()) -} diff --git a/examples/sms_form_dag.go b/examples/sms_form_dag.go deleted file mode 100644 index 316f164..0000000 --- a/examples/sms_form_dag.go +++ /dev/null @@ -1,708 +0,0 @@ -package main - -import ( - "context" - "fmt" - "regexp" - "strings" - "time" - - "github.com/oarkflow/json" - - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/utils" - - "github.com/oarkflow/jet" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/consts" -) - -func main() { - flow := dag.NewDAG("SMS Sender", "sms-sender", func(taskID string, result mq.Result) { - fmt.Printf("SMS workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content"))) - }) - - // Add SMS workflow nodes - // Note: Page nodes have no timeout by default, allowing users unlimited time for form input - flow.AddNode(dag.Page, "SMS Form", "SMSForm", &SMSFormNode{}) - flow.AddNode(dag.Function, "Validate Input", "ValidateInput", &ValidateInputNode{}) - flow.AddNode(dag.Function, "Send SMS", "SendSMS", &SendSMSNode{}) - flow.AddNode(dag.Page, "SMS Result", "SMSResult", &SMSResultNode{}) - flow.AddNode(dag.Page, "Error Page", "ErrorPage", &ErrorPageNode{}) - - // Define edges for SMS workflow - flow.AddEdge(dag.Simple, "Form to Validation", "SMSForm", "ValidateInput") - flow.AddCondition("ValidateInput", map[string]string{"valid": "SendSMS", "invalid": "ErrorPage"}) - flow.AddCondition("SendSMS", map[string]string{"sent": "SMSResult", "failed": "ErrorPage"}) - - // Start the flow - if flow.Error != nil { - panic(flow.Error) - } - - fmt.Println("Starting SMS DAG server on http://0.0.0.0:8083") - fmt.Println("Navigate to the URL to access the SMS form") - flow.Start(context.Background(), "0.0.0.0:8083") -} - -// SMSFormNode - Initial form to collect SMS data -type SMSFormNode struct { - dag.Operation -} - -func (s *SMSFormNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - // Check if this is a form submission - var inputData map[string]any - if task.Payload != nil && len(task.Payload) > 0 { - if err := json.Unmarshal(task.Payload, &inputData); err == nil { - // If we have valid input data, pass it through for validation - return mq.Result{Payload: task.Payload, Ctx: ctx} - } - } - - // Otherwise, show the form - htmlTemplate := ` - - - - SMS Sender - - - -
-

📱 SMS Sender

-
-

Send SMS messages through our secure DAG workflow

-
-
-
- - -
- Supports US format: +1234567890 or 1234567890 -
-
- -
- - -
0/160 characters
-
- -
- - -
- - -
-
- - - -` - - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(htmlTemplate, map[string]any{ - "task_id": ctx.Value("task_id"), - }) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - data := map[string]any{ - "html_content": rs, - "step": "form", - } - bt, _ := json.Marshal(data) - return mq.Result{Payload: bt, Ctx: ctx} -} - -// ValidateInputNode - Validates phone number and message -type ValidateInputNode struct { - dag.Operation -} - -func (v *ValidateInputNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{ - Error: fmt.Errorf("invalid input data: %v", err), - Ctx: ctx, - } - } - - // Extract form data - phone, _ := inputData["phone"].(string) - message, _ := inputData["message"].(string) - senderName, _ := inputData["sender_name"].(string) - - // Validate phone number - if phone == "" { - inputData["validation_error"] = "Phone number is required" - inputData["error_field"] = "phone" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Clean and validate phone number format - cleanPhone := regexp.MustCompile(`\D`).ReplaceAllString(phone, "") - - // Check for valid US phone number (10 or 11 digits) - if len(cleanPhone) == 10 { - cleanPhone = "1" + cleanPhone // Add country code - } else if len(cleanPhone) != 11 || !strings.HasPrefix(cleanPhone, "1") { - inputData["validation_error"] = "Invalid phone number format. Please use US format: +1234567890 or 1234567890" - inputData["error_field"] = "phone" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Validate message - if message == "" { - inputData["validation_error"] = "Message is required" - inputData["error_field"] = "message" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - if len(message) > 160 { - inputData["validation_error"] = "Message too long. Maximum 160 characters allowed" - inputData["error_field"] = "message" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - - // Check for potentially harmful content - forbiddenWords := []string{"spam", "scam", "fraud", "hack"} - messageLower := strings.ToLower(message) - for _, word := range forbiddenWords { - if strings.Contains(messageLower, word) { - inputData["validation_error"] = "Message contains prohibited content" - inputData["error_field"] = "message" - bt, _ := json.Marshal(inputData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"} - } - } - - // All validations passed - validatedData := map[string]any{ - "phone": cleanPhone, - "message": message, - "sender_name": senderName, - "validated_at": time.Now().Format("2006-01-02 15:04:05"), - "validation_status": "success", - "formatted_phone": formatPhoneForDisplay(cleanPhone), - "char_count": len(message), - } - - bt, _ := json.Marshal(validatedData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"} -} - -// SendSMSNode - Simulates sending SMS -type SendSMSNode struct { - dag.Operation -} - -func (s *SendSMSNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - phone, _ := inputData["phone"].(string) - message, _ := inputData["message"].(string) - senderName, _ := inputData["sender_name"].(string) - - // Simulate SMS sending delay - time.Sleep(500 * time.Millisecond) - - // Simulate occasional failures for demo purposes - timestamp := time.Now() - success := timestamp.Second()%10 != 0 // 90% success rate - - if !success { - errorData := map[string]any{ - "phone": phone, - "message": message, - "sender_name": senderName, - "sms_status": "failed", - "error_message": "SMS gateway temporarily unavailable. Please try again.", - "sent_at": timestamp.Format("2006-01-02 15:04:05"), - "retry_suggested": true, - } - bt, _ := json.Marshal(errorData) - return mq.Result{ - Payload: bt, - Ctx: ctx, - ConditionStatus: "failed", - } - } - - // Generate mock SMS ID and response - smsID := fmt.Sprintf("SMS_%d_%s", timestamp.Unix(), phone[len(phone)-4:]) - - resultData := map[string]any{ - "phone": phone, - "formatted_phone": formatPhoneForDisplay(phone), - "message": message, - "sender_name": senderName, - "sms_status": "sent", - "sms_id": smsID, - "sent_at": timestamp.Format("2006-01-02 15:04:05"), - "delivery_estimate": "1-2 minutes", - "cost_estimate": "$0.02", - "gateway": "MockSMS Gateway", - "char_count": len(message), - } - - fmt.Printf("📱 SMS sent successfully! ID: %s, Phone: %s\n", smsID, formatPhoneForDisplay(phone)) - - bt, _ := json.Marshal(resultData) - return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"} -} - -// SMSResultNode - Shows successful SMS result -type SMSResultNode struct { - dag.Operation -} - -func (r *SMSResultNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - htmlTemplate := ` - - - - SMS Sent Successfully - - - -
-
-

SMS Sent Successfully!

- -
{{sms_status}}
- -
-
-
📱 Phone Number
-
{{formatted_phone}}
-
-
-
🆔 SMS ID
-
{{sms_id}}
-
-
-
⏰ Sent At
-
{{sent_at}}
-
-
-
🚚 Delivery
-
{{delivery_estimate}}
-
- {{if sender_name}} -
-
👤 Sender
-
{{sender_name}}
-
- {{end}} -
-
💰 Cost
-
{{cost_estimate}}
-
-
- -
-
💬 Message Sent ({{char_count}} chars):
-
- "{{message}}" -
-
- -
- 📱 Send Another SMS - 📊 View Metrics -
- -
- Gateway: {{gateway}} | Task completed in DAG workflow -
-
- -` - - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(htmlTemplate, inputData) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - finalData := map[string]any{ - "html_content": rs, - "result": inputData, - "step": "success", - } - bt, _ := json.Marshal(finalData) - return mq.Result{Payload: bt, Ctx: ctx} -} - -// ErrorPageNode - Shows validation or sending errors -type ErrorPageNode struct { - dag.Operation -} - -func (e *ErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var inputData map[string]any - if err := json.Unmarshal(task.Payload, &inputData); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - // Determine error type and message - errorMessage, _ := inputData["validation_error"].(string) - errorField, _ := inputData["error_field"].(string) - smsError, _ := inputData["error_message"].(string) - - if errorMessage == "" && smsError != "" { - errorMessage = smsError - errorField = "sms_sending" - } - if errorMessage == "" { - errorMessage = "An unknown error occurred" - } - - htmlTemplate := ` - - - - SMS Error - - - -
-
-

SMS Error

- -
- {{error_message}} -
- - {{if error_field}} -
- Error Field: {{error_field}}
- Action Required: Please correct the highlighted field and try again. -
- {{end}} - - {{if retry_suggested}} -
- ⚠️ Temporary Issue: This appears to be a temporary gateway issue. - Please try sending your SMS again in a few moments. -
- {{end}} - -
- 🔄 Try Again - 📊 Check Status -
- -
- DAG Error Handler | SMS Workflow Failed -
-
- -` - - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - templateData := map[string]any{ - "error_message": errorMessage, - "error_field": errorField, - "retry_suggested": inputData["retry_suggested"], - } - - rs, err := parser.ParseTemplate(htmlTemplate, templateData) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - finalData := map[string]any{ - "html_content": rs, - "error_data": inputData, - "step": "error", - } - bt, _ := json.Marshal(finalData) - return mq.Result{Payload: bt, Ctx: ctx} -} - -// Helper function to format phone number for display -func formatPhoneForDisplay(phone string) string { - if len(phone) == 11 && strings.HasPrefix(phone, "1") { - // Format as +1 (XXX) XXX-XXXX - return fmt.Sprintf("+1 (%s) %s-%s", - phone[1:4], - phone[4:7], - phone[7:11]) - } - return phone -} diff --git a/examples/static.go b/examples/static.go deleted file mode 100644 index b1144fb..0000000 --- a/examples/static.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "log" - "mime" - "os" - "path/filepath" - "strings" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/rewrite" -) - -type Config struct { - Prefix string `json:"prefix"` - Root string `json:"root"` - Index string `json:"index"` - UseIndex bool `json:"use_index"` - Compress bool `json:"compress"` -} - -func main() { - app := fiber.New() - config := Config{ - Prefix: "/data", - Root: "/Users/sujit/Sites/mq/examples/webroot", - UseIndex: true, - Compress: true, - } - New(app, config) - log.Fatal(app.Listen(":3000")) -} - -func New(router fiber.Router, cfg ...Config) { - var config Config - if len(cfg) > 0 { - config = cfg[0] - } - if config.Root == "" { - config.Root = "./" - } - if config.Prefix == "/" { - config.Prefix = "" - } - if config.UseIndex && config.Index == "" { - config.Index = "index.html" - } - if config.Compress { - router.Use(compress.New(compress.Config{ - Level: compress.LevelBestSpeed, - })) - } - rules := make(map[string]string) - root := filepath.Clean(config.Root) - filepath.WalkDir(config.Root, func(path string, d os.DirEntry, err error) error { - if !d.IsDir() { - path = strings.TrimPrefix(path, root) - rules[path] = filepath.Join(config.Prefix, path) - } - return nil - }) - router.Use(rewrite.New(rewrite.Config{Rules: rules})) - router.Get(config.Prefix+"/*", handleStaticFile(config)) -} - -func handleStaticFile(config Config) fiber.Handler { - return func(c *fiber.Ctx) error { - fullPath := c.Params("*") - filePath := filepath.Join(config.Root, fullPath) - fileInfo, err := os.Stat(filePath) - if err != nil { - return c.Status(fiber.StatusNotFound).SendString("File not found") - } - if fileInfo.IsDir() { - if !config.UseIndex { - return c.Status(fiber.StatusNotFound).SendString("Invalid file") - } - filePath = filepath.Join(filePath, config.Index) - } - fileContent, err := os.ReadFile(filePath) - if err != nil { - return c.Status(fiber.StatusNotFound).SendString("File not found") - } - ext := filepath.Ext(filePath) - mimeType := mime.TypeByExtension(ext) - if mimeType == "" { - mimeType = "application/octet-stream" - } - c.Set("Content-Type", mimeType) - c.Set("Cache-Control", "public, max-age=31536000") - return c.Send(fileContent) - } -} diff --git a/examples/tasks/operations.go b/examples/tasks/operations.go deleted file mode 100644 index 9f1513e..0000000 --- a/examples/tasks/operations.go +++ /dev/null @@ -1,135 +0,0 @@ -package tasks - -import ( - "context" - - "github.com/oarkflow/json" - - v2 "github.com/oarkflow/mq/dag" - - "github.com/oarkflow/mq" -) - -type GetData struct { - v2.Operation -} - -func (e *GetData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return mq.Result{Payload: task.Payload, Ctx: ctx} -} - -type Loop struct { - v2.Operation -} - -func (e *Loop) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - return mq.Result{Payload: task.Payload, Ctx: ctx} -} - -type Condition struct { - v2.Operation -} - -func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - switch email := data["email"].(type) { - case string: - if email == "abc.xyz@gmail.com" { - return mq.Result{Payload: task.Payload, ConditionStatus: "pass", Ctx: ctx} - } - return mq.Result{Payload: task.Payload, ConditionStatus: "fail", Ctx: ctx} - default: - return mq.Result{Payload: task.Payload, ConditionStatus: "fail", Ctx: ctx} - } -} - -type PrepareEmail struct { - v2.Operation -} - -func (e *PrepareEmail) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - data["email_valid"] = true - d, _ := json.Marshal(data) - return mq.Result{Payload: d, Ctx: ctx} -} - -type EmailDelivery struct { - v2.Operation -} - -func (e *EmailDelivery) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - data["email_sent"] = true - d, _ := json.Marshal(data) - return mq.Result{Payload: d, Ctx: ctx} -} - -type SendSms struct { - v2.Operation -} - -func (e *SendSms) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - data["sms_sent"] = true - d, _ := json.Marshal(data) - return mq.Result{Payload: d, Ctx: ctx} -} - -type StoreData struct { - v2.Operation -} - -func (e *StoreData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - data["stored"] = true - d, _ := json.Marshal(data) - return mq.Result{Payload: d, Ctx: ctx} -} - -type InAppNotification struct { - v2.Operation -} - -func (e *InAppNotification) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - err := json.Unmarshal(task.Payload, &data) - if err != nil { - panic(err) - } - data["notified"] = true - d, _ := json.Marshal(data) - return mq.Result{Payload: d, Ctx: ctx} -} - -type Final struct { - v2.Operation -} - -func (e *Final) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - rs := map[string]any{ - "html_content": `Processed successfully!`, - } - bt, _ := json.Marshal(rs) - return mq.Result{Payload: bt, Ctx: ctx} -} diff --git a/examples/tasks/scheduler.go b/examples/tasks/scheduler.go deleted file mode 100644 index f913926..0000000 --- a/examples/tasks/scheduler.go +++ /dev/null @@ -1,20 +0,0 @@ -package tasks - -import ( - "context" - "fmt" - - "github.com/oarkflow/mq" -) - -func SchedulerHandler(ctx context.Context, task *mq.Task) mq.Result { - fmt.Printf("Processing task: %s\n", task.ID) - return mq.Result{Error: nil} -} - -func SchedulerCallback(ctx context.Context, result mq.Result) error { - if result.Error != nil { - fmt.Println("Task failed!", result.Error.Error()) - } - return nil -} diff --git a/examples/tasks/tasks.go b/examples/tasks/tasks.go deleted file mode 100644 index aac868f..0000000 --- a/examples/tasks/tasks.go +++ /dev/null @@ -1,110 +0,0 @@ -package tasks - -import ( - "context" - "fmt" - "log" - - "github.com/oarkflow/json" - - v2 "github.com/oarkflow/mq/dag" - - "github.com/oarkflow/mq" -) - -type Node1 struct{ v2.Operation } - -func (t *Node1) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - fmt.Println("Node 1", string(task.Payload)) - return mq.Result{Payload: task.Payload, TaskID: task.ID} -} - -type Node2 struct{ v2.Operation } - -func (t *Node2) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - fmt.Println("Node 2", string(task.Payload)) - return mq.Result{Payload: task.Payload, TaskID: task.ID} -} - -type Node3 struct{ v2.Operation } - -func (t *Node3) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - fmt.Println(string(task.Payload)) - err := json.Unmarshal(task.Payload, &user) - if err != nil { - panic(err) - } - age := int(user["age"].(float64)) - status := "FAIL" - if age > 20 { - status = "PASS" - } - user["status"] = status - resultPayload, _ := json.Marshal(user) - return mq.Result{Payload: resultPayload, ConditionStatus: status} -} - -type Node4 struct{ v2.Operation } - -func (t *Node4) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - _ = json.Unmarshal(task.Payload, &user) - user["node"] = "D" - resultPayload, _ := json.Marshal(user) - return mq.Result{Payload: resultPayload} -} - -type Node5 struct{ v2.Operation } - -func (t *Node5) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - _ = json.Unmarshal(task.Payload, &user) - user["node"] = "E" - resultPayload, _ := json.Marshal(user) - return mq.Result{Payload: resultPayload} -} - -type Node6 struct{ v2.Operation } - -func (t *Node6) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - _ = json.Unmarshal(task.Payload, &user) - resultPayload, _ := json.Marshal(map[string]any{"storage": user}) - return mq.Result{Payload: resultPayload} -} - -type Node7 struct{ v2.Operation } - -func (t *Node7) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - _ = json.Unmarshal(task.Payload, &user) - user["node"] = "G" - resultPayload, _ := json.Marshal(user) - return mq.Result{Payload: resultPayload} -} - -type Node8 struct{ v2.Operation } - -func (t *Node8) ProcessTask(_ context.Context, task *mq.Task) mq.Result { - var user map[string]any - _ = json.Unmarshal(task.Payload, &user) - user["node"] = "H" - resultPayload, _ := json.Marshal(user) - return mq.Result{Payload: resultPayload} -} - -func Callback(_ context.Context, task mq.Result) mq.Result { - fmt.Println("Received task", task.TaskID, "Payload", string(task.Payload), task.Error, task.Topic) - return mq.Result{} -} - -func NotifyResponse(_ context.Context, result mq.Result) error { - log.Printf("DAG - FINAL_RESPONSE ~> TaskID: %s, Payload: %s, Topic: %s, Error: %v, Latency: %s", result.TaskID, result.Payload, result.Topic, result.Error, result.Latency) - return nil -} - -func NotifySubDAGResponse(_ context.Context, result mq.Result) error { - log.Printf("SUB DAG - FINAL_RESPONSE ~> TaskID: %s, Payload: %s, Topic: %s, Error: %v, Latency: %s", result.TaskID, result.Payload, result.Topic, result.Error, result.Latency) - return nil -} diff --git a/examples/v2.go b/examples/v2.go deleted file mode 100644 index 2db4ed9..0000000 --- a/examples/v2.go +++ /dev/null @@ -1,189 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/oarkflow/json" - - "github.com/gofiber/fiber/v2" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" - - "github.com/oarkflow/jet" - - "github.com/oarkflow/mq/consts" -) - -type Form struct { - dag.Operation -} - -func (p *Form) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - baseURI := "" - if dg, ok := task.GetFlow().(*dag.DAG); ok { - baseURI = dg.BaseURI() - } - bt, err := os.ReadFile("webroot/form.html") - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(string(bt), map[string]any{ - "task_id": ctx.Value("task_id"), - "base_uri": baseURI, - }) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - data := map[string]any{ - "html_content": rs, - } - bt, _ = json.Marshal(data) - return mq.Result{Payload: bt, Ctx: ctx} -} - -type NodeA struct { - dag.Operation -} - -func (p *NodeA) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - data["allowed_voting"] = data["age"] == "18" - updatedPayload, _ := json.Marshal(data) - return mq.Result{Payload: updatedPayload, Ctx: ctx} -} - -type NodeB struct { - dag.Operation -} - -func (p *NodeB) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - data["female_voter"] = data["gender"] == "female" - updatedPayload, _ := json.Marshal(data) - return mq.Result{Payload: updatedPayload, Ctx: ctx} -} - -type NodeC struct { - dag.Operation -} - -func (p *NodeC) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - var data map[string]any - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - data["voted"] = true - updatedPayload, _ := json.Marshal(data) - return mq.Result{Payload: updatedPayload, Ctx: ctx} -} - -type Result struct { - dag.Operation -} - -func (p *Result) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { - baseURI := "" - if dg, ok := task.GetFlow().(*dag.DAG); ok { - baseURI = dg.BaseURI() - } - bt, err := os.ReadFile("webroot/result.html") - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - data := map[string]any{ - "base_uri": baseURI, - } - if task.Payload != nil { - if err := json.Unmarshal(task.Payload, &data); err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - } - if bt != nil { - parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) - rs, err := parser.ParseTemplate(string(bt), data) - if err != nil { - return mq.Result{Error: err, Ctx: ctx} - } - ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) - data := map[string]any{ - "html_content": rs, - } - bt, _ := json.Marshal(data) - return mq.Result{Payload: bt, Ctx: ctx} - } - return mq.Result{Payload: task.Payload, Ctx: ctx} -} - -// RemoveHTMLContent recursively removes the "html_content" field from the given JSON. -func RemoveHTMLContent(data json.RawMessage, field string) (json.RawMessage, error) { - var result interface{} - if err := json.Unmarshal(data, &result); err != nil { - return nil, err - } - removeField(result, field) - return json.Marshal(result) -} - -// removeField recursively traverses the structure and removes "html_content" field. -func removeField(v interface{}, field string) { - switch v := v.(type) { - case map[string]interface{}: - // Check if the field is in the map and remove it. - delete(v, field) - // Recursively remove the field from nested objects. - for _, value := range v { - removeField(value, field) - } - case []interface{}: - // If it's an array, recursively process each item. - for _, item := range v { - removeField(item, field) - } - } -} - -func notify(taskID string, result mq.Result) { - filteredData, err := RemoveHTMLContent(result.Payload, "html_content") - if err != nil { - panic(err) - } - fmt.Printf("Final result for task %s: %s, status: %s, latency: %s\n", taskID, string(filteredData), result.Status, result.Latency) -} - -func main() { - flow := dag.NewDAG("Sample DAG", "sample-dag", notify, mq.WithBrokerURL(":8083"), mq.WithHTTPApi(true)) - flow.AddNode(dag.Page, "Form", "Form", &Form{}) - flow.AddNode(dag.Function, "NodeA", "NodeA", &NodeA{}) - flow.AddNode(dag.Function, "NodeB", "NodeB", &NodeB{}) - flow.AddNode(dag.Function, "NodeC", "NodeC", &NodeC{}) - flow.AddNode(dag.Page, "Result", "Result", &Result{}) - flow.AddEdge(dag.Simple, "Form", "Form", "NodeA") - flow.AddEdge(dag.Simple, "NodeA", "NodeA", "NodeB") - flow.AddEdge(dag.Simple, "NodeB", "NodeB", "NodeC") - flow.AddEdge(dag.Simple, "NodeC", "NodeC", "Result") - dag.AddHandler("Form", func(s string) mq.Processor { - opt := dag.Operation{ - Tags: []string{"built-in", "form"}, - } - return &Form{Operation: opt} - }) - fmt.Println(dag.AvailableHandlers()) - if flow.Error != nil { - panic(flow.Error) - } - app := fiber.New() - flowApp := app.Group("/") - flow.Handlers(flowApp, "/") - app.Listen(":8082") -} diff --git a/examples/webroot/css/app.css b/examples/webroot/css/app.css deleted file mode 100644 index fdfe2e2..0000000 --- a/examples/webroot/css/app.css +++ /dev/null @@ -1,25 +0,0 @@ -#container{ - width: 400px; - margin: 30px auto 0 auto; -} - -#messages{ - height: 200px; - overflow-y: scroll; -} - -.control-item{ - margin-top: 15px; -} - -#message-send-btn, #clear-messages-btn{ - width: 100%; -} - -#message-list{ - padding-left: 25px; -} - -.message-item{ - margin-bottom: 8px; -} \ No newline at end of file diff --git a/examples/webroot/favicon.ico b/examples/webroot/favicon.ico deleted file mode 100644 index e69de29..0000000 diff --git a/examples/webroot/form.html b/examples/webroot/form.html deleted file mode 100644 index 6484306..0000000 --- a/examples/webroot/form.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - User Data Form - - - -

Enter Your Information

-
-
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
- - - - diff --git a/examples/webroot/index.html b/examples/webroot/index.html deleted file mode 100644 index 662bb0e..0000000 --- a/examples/webroot/index.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - Task Status Dashboard - - - - - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - -
-
-

Table

-
- - - - - - - - - - - - - - -
Task IDCreated AtProcessed AtLatencyStatusView
-
-
-
-
-
-
-
-
-
-

DAG Image view per task

-
- -
-
-
-

Node Result:

-
-

Node Result per task

-
-
-
- -
-
-
-
- - - - - diff --git a/examples/webroot/js/app.js b/examples/webroot/js/app.js deleted file mode 100644 index a3791af..0000000 --- a/examples/webroot/js/app.js +++ /dev/null @@ -1,186 +0,0 @@ -(function(SS) { - 'use strict'; - let uiContent - function loadSVG(url) { - fetch(url) - .then(response => response.text()) - .then(svgContent => { - const container = document.getElementById('svg-container'); - container.src = url; - uiContent = svgContent; - }) - .catch(err => console.error('Failed to load SVG:', err)); - } - - window.onload = function() { - loadSVG('/ui'); - }; - document.getElementById('send-request').addEventListener('click', function() { - const input = document.getElementById('payload'); - const payloadData = JSON.parse(input.value); - const data = { payload: payloadData }; - - fetch('/request', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) - .then(response => { - const contentType = response.headers.get('Content-Type'); - if (contentType && contentType.includes("text/html")) { - return response.text().then(html => ({ html })); - } else { - return response.json(); - } - }) - .then(data => { - if (data.html) { - const el = document.getElementById("response"); - el.innerHTML = data.html; - } else { - console.log("Success", data); - } - }) - .catch(error => console.error('Error:', error)); - - }); - - const tasks = {}; - - function attachSVGNodeEvents() { - const svgNodes = document.querySelectorAll('g.node'); // Adjust selector as per your SVG structure - svgNodes.forEach(node => { - node.classList.add("cursor-pointer") - node.addEventListener('click', handleSVGNodeClick); - }); - } - - // Function to handle the click on an SVG node and show the popover - function handleSVGNodeClick(event) { - const nodeId = event.currentTarget.id; // Get the node ID (e.g., 'node_store:data') - const nodeData = findNodeDataById(nodeId); // Fetch data related to the node (status, result, etc.) - if (nodeData) { - showSVGPopover(event, nodeData); - } - } - - // Function to show the popover next to the clicked SVG node - function showSVGPopover(event, nodeData) { - const popover = document.getElementById('svg-popover'); - document.getElementById('task-id').innerHTML = nodeData.task_id - popover.classList.add('visible'); - popover.innerHTML = ` -
-
- Node: - ${nodeData.topic} - - Status: - ${nodeData.status} - - Result: -
${JSON.stringify(nodeData.payload, null, 2)}
-                        
- - Error: - ${nodeData.error || 'N/A'} - - Created At: - ${nodeData.created_at} - - Processed At: - ${nodeData.processed_at} - - Latency: - ${nodeData.latency} -
-
- - `; - } - - // Function to find node data (status, result, error) by the node's ID - function findNodeDataById(nodeId) { - for (const taskId in tasks) { - const task = tasks[taskId]; - const node = task.nodes.find(n => `node_${n.topic}` === nodeId); // Ensure the ID format matches your SVG - if (node) { - return node; - } - } - return null; // Return null if no matching node is found - } - - function addOrUpdateTask(message, isFinal = false) { - const taskTableBody = document.getElementById('taskTableBody'); - const taskId = message.task_id; - const rowId = `row-${taskId}`; - let existingRow = document.getElementById(rowId); - - if (!existingRow) { - const row = document.createElement('tr'); - row.id = rowId; - taskTableBody.insertBefore(row, taskTableBody.firstChild); - existingRow = row; - } - - tasks[taskId] = tasks[taskId] || { nodes: [], final: null }; - if (isFinal) tasks[taskId].final = message; - else tasks[taskId].nodes.push(message); - - const latestStatus = isFinal ? message.status : message.status; - const statusColor = latestStatus === 'success' ? 'bg-green-100 text-green-700' : - latestStatus === 'fail' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'; - - existingRow.innerHTML = ` - ${taskId} - ${new Date(message.created_at).toLocaleString()} - ${new Date(message.processed_at).toLocaleString()} - ${message.latency} - ${latestStatus} - - - - `; - - attachViewButtonEvent(); - } - - function attachViewButtonEvent() { - const buttons = document.querySelectorAll('.view-btn'); - buttons.forEach(button => { - button.removeEventListener('click', handleViewButtonClick); - button.addEventListener('click', handleViewButtonClick); - }); - } - - function handleViewButtonClick(event) { - document.getElementById("task-svg").innerHTML = uiContent - attachSVGNodeEvents(); - const taskId = event.target.getAttribute('data-task-id'); - const task = tasks[taskId]; - updateSVGNodes(task); - } - - function updateSVGNodes(task) { - console.log(task) - task.nodes.forEach((node) => { - const svgNode = document.querySelector(`#node_${node.topic.replace(':', '\\:')}`); - console.log(svgNode) - if (svgNode) { - const fillColor = node.status === 'success' ? '#A5D6A7' : node.status === 'fail' ? '#EF9A9A' : '#FFE082'; - const path = svgNode.querySelector('path'); - if (path) path.setAttribute('fill', fillColor); - } - }); - } - - - let ss = new SS('ws://' + window.location.host + '/notify'); - ss.onConnect(() => ss.emit('join', "global")); - ss.onDisconnect(() => alert('chat disconnected')); - ss.on('message', msg => addOrUpdateTask(msg, false)); - ss.on('final-message', msg => addOrUpdateTask(msg, true)); -})(window.SS); \ No newline at end of file diff --git a/examples/webroot/js/socket.js b/examples/webroot/js/socket.js deleted file mode 100644 index c990810..0000000 --- a/examples/webroot/js/socket.js +++ /dev/null @@ -1,233 +0,0 @@ -if (typeof window === 'undefined') { - var window = {}; -} -if (typeof module === 'undefined') { - var module = {}; -} -(function (window, module) { - 'use strict'; - var SS = function (url, opts) { - opts = opts || {}; - - var self = this, - events = {}, - reconnectOpts = { enabled: true, replayOnConnect: true, intervalMS: 5000 }, - reconnecting = false, - connectedOnce = false, - headerStartCharCode = 1, - headerStartChar = String.fromCharCode(headerStartCharCode), - dataStartCharCode = 2, - dataStartChar = String.fromCharCode(dataStartCharCode), - subProtocol = 'sac-sock', - ws = new WebSocket(url, subProtocol); - - //blomp blomp-a noop noop a-noop noop noop - self.noop = function () { }; - - //we really only support reconnect options for now - if (typeof opts.reconnectOpts == 'object') { - for (var i in opts.reconnectOpts) { - if (!opts.reconnectOpts.hasOwnProperty(i)) continue; - reconnectOpts[i] = opts.reconnectOpts[i]; - } - } - - //sorry, only supporting arraybuffer at this time - //maybe if there is demand for it, I'll add Blob support - ws.binaryType = 'arraybuffer'; - - //Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored. - ws.onmessage = function (e) { - var msg = e.data, - headers = {}, - eventName = '', - data = '', - chr = null, - i, msgLen; - - if (typeof msg === 'string') { - var dataStarted = false, - headerStarted = false; - - for (i = 0, msgLen = msg.length; i < msgLen; i++) { - chr = msg[i]; - if (!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar) { - eventName += chr; - } else if (!headerStarted && chr === headerStartChar) { - headerStarted = true; - } else if (headerStarted && !dataStarted && chr !== dataStartChar) { - headers[chr] = true; - } else if (!dataStarted && chr === dataStartChar) { - dataStarted = true; - } else { - data += chr; - } - } - } else if (msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined) { - var dv = new DataView(msg), - headersStarted = false; - - for (i = 0, msgLen = dv.byteLength; i < msgLen; i++) { - chr = dv.getUint8(i); - - if (chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted) { - eventName += String.fromCharCode(chr); - } else if (chr === headerStartCharCode && !headersStarted) { - headersStarted = true; - } else if (headersStarted && chr !== dataStartCharCode) { - headers[String.fromCharCode(chr)] = true; - } else if (chr === dataStartCharCode) { - data = dv.buffer.slice(i + 1); - break; - } - } - } - - if (eventName.length === 0) return; //no event to dispatch - if (typeof events[eventName] === 'undefined') return; - events[eventName].call(self, (headers.J) ? JSON.parse(data) : data); - }; - - /** - * startReconnect is an internal function for reconnecting after an unexpected disconnect - * - * @function startReconnect - * - */ - function startReconnect() { - setTimeout(function () { - console.log('attempting reconnect'); - var newWS = new WebSocket(url, subProtocol); - newWS.onmessage = ws.onmessage; - newWS.onclose = ws.onclose; - newWS.binaryType = ws.binaryType; - - //we need to run the initially set onConnect function on first successful connect, - //even if replayOnConnect is disabled. The server might not be available on first - //connection attempt. - if (reconnectOpts.replayOnConnect || !connectedOnce) { - newWS.onopen = ws.onopen; - } - ws = newWS; - if (!reconnectOpts.replayOnConnect && connectedOnce) { - self.onConnect(self.noop); - } - }, reconnectOpts.intervalMS); - } - - /** - * onConnect registers a callback to be run when the websocket connection is open. - * - * @method onConnect - * @param {Function} callback(event) - The callback that will be executed when the websocket connection opens. - * - */ - self.onConnect = function (callback) { - ws.onopen = function () { - connectedOnce = true; - var args = arguments; - callback.apply(self, args); - if (reconnecting) { - reconnecting = false; - } - }; - }; - self.onConnect(self.noop); - - /** - * onDisconnect registers a callback to be run when the websocket connection is closed. - * - * @method onDisconnect - * @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed. - */ - self.onDisconnect = function (callback) { - ws.onclose = function () { - var args = arguments; - if (!reconnecting && connectedOnce) { - callback.apply(self, args); - } - if (reconnectOpts.enabled) { - reconnecting = true; - startReconnect(); - } - }; - }; - self.onDisconnect(self.noop); - - /** - * on registers an event to be called when the client receives an emit from the server for - * the given eventName. - * - * @method on - * @param {String} eventName - The name of the event being registerd - * @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer - * - */ - self.on = function (eventName, callback) { - events[eventName] = callback; - }; - - /** - * off unregisters an emit event - * - * @method off - * @param {String} eventName - The name of event being unregistered - */ - self.off = function (eventName) { - if (events[eventName]) { - delete events[eventName]; - } - }; - - /** - * emit dispatches an event to the server - * - * @method emit - * @param {String} eventName - The event to dispatch - * @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload. - */ - self.emit = function (eventName, data) { - var rs = ws.readyState; - if (rs === 0) { - console.warn("websocket is not open yet"); - return; - } else if (rs === 2 || rs === 3) { - console.error("websocket is closed"); - return; - } - var msg = ''; - if (data instanceof ArrayBuffer) { - var ab = new ArrayBuffer(data.byteLength + eventName.length + 1), - newBuf = new DataView(ab), - oldBuf = new DataView(data), - i = 0; - for (var evtLen = eventName.length; i < evtLen; i++) { - newBuf.setUint8(i, eventName.charCodeAt(i)); - } - newBuf.setUint8(i, dataStartCharCode); - i++; - for (var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++) { - newBuf.setUint8(i, oldBuf.getUint8(x)); - } - msg = ab; - } else if (typeof data === 'object') { - msg = eventName + dataStartChar + JSON.stringify(data); - } else { - msg = eventName + dataStartChar + data; - } - ws.send(msg); - }; - - /** - * close will close the websocket connection, calling the "onDisconnect" event if one has been registered. - * - * @method close - */ - self.close = function () { - reconnectOpts.enabled = false; //don't reconnect if close is called - return ws.close(1000); - }; - }; - window.SS = SS; - module.exports = SS; -})(window, module); diff --git a/examples/webroot/result.html b/examples/webroot/result.html deleted file mode 100644 index 7ea68e3..0000000 --- a/examples/webroot/result.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - Task Result - - - - -

Task Result

- -
-

Loading result...

-
- - - - - - - diff --git a/go.mod b/go.mod index 7ffef39..7ae18dd 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,9 @@ require ( github.com/oarkflow/form v0.0.0-20241203111156-b1be5636af43 github.com/oarkflow/jet v0.0.4 github.com/oarkflow/json v0.0.28 - github.com/oarkflow/log v1.0.79 + github.com/oarkflow/log v1.0.83 github.com/oarkflow/xid v1.2.8 - golang.org/x/crypto v0.33.0 + golang.org/x/crypto v0.41.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/time v0.11.0 ) @@ -25,7 +25,7 @@ require ( github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect github.com/kaptinlin/go-i18n v0.1.4 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.28.0 // indirect ) require ( @@ -40,5 +40,5 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index 782da54..29a1f86 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/oarkflow/jsonschema v0.0.4 h1:n5Sb7WVb7NNQzn/ei9++4VPqKXCPJhhsHeTGJkI github.com/oarkflow/jsonschema v0.0.4/go.mod h1:AxNG3Nk7KZxnnjRJlHLmS1wE9brtARu5caTFuicCtnA= github.com/oarkflow/log v1.0.79 h1:DxhtkBGG+pUu6cudSVw5g75FbKEQJkij5w7n5AEN00M= github.com/oarkflow/log v1.0.79/go.mod h1:U/4chr1DyOiQvS6JiQpjYTCJhK7RGR8xrXPsGlouLzM= +github.com/oarkflow/log v1.0.83 h1:T/38wvjuNeVJ9PDo0wJDTnTUQZ5XeqlcvpbCItuFFJo= +github.com/oarkflow/log v1.0.83/go.mod h1:dMn57z9uq11Y264cx9c9Ac7ska9qM+EBhn4qf9CNlsM= github.com/oarkflow/xid v1.2.8 h1:uCIX61Binq2RPMsqImZM6pPGzoZTmRyD6jguxF9aAA0= github.com/oarkflow/xid v1.2.8/go.mod h1:jG4YBh+swbjlWApGWDBYnsJEa7hi3CCpmuqhB3RAxVo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -65,13 +67,19 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/services/examples/app/data/login_output.json b/services/examples/app/data/login_output.json deleted file mode 100644 index 7e3f3da..0000000 --- a/services/examples/app/data/login_output.json +++ /dev/null @@ -1,4 +0,0 @@ -{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345053712301170690\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345053712301170690","username":"admin"} -{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345054073104531457\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345054073104531457","username":"admin"} -{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"/process?task_id=345055067016990721\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345055067016990721","username":"admin"} -{"html_content":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003chead\u003e\n \u003ctitle\u003eBasic Template\u003c/title\u003e\n \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n \u003clink rel=\"stylesheet\" href=\"form.css\"\u003e\n \u003cstyle\u003e\n .required {\n color: #dc3545;\n }\n\n .group-header {\n font-weight: bold;\n margin-top: 0.5rem;\n margin-bottom: 0.5rem;\n }\n\n .section-title {\n color: #0d6efd;\n border-bottom: 2px solid #0d6efd;\n padding-bottom: 0.5rem;\n }\n\n .form-group-fields\u003ediv {\n margin-bottom: 1rem;\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\n\u003cbody class=\"bg-gray-100\"\u003e\n \u003cform class=\"form-horizontal\" action=\"{{current_uri}}?task_id=345181241327013889\u0026next=true\" method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cdiv class=\"form-container p-4 bg-white shadow-md rounded\"\u003e\n \n\u003cdiv class=\"form-group-container\"\u003e\n\t\n\t\u003cdiv class=\"form-group-fields\"\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"username\"\u003eUsername or Email \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"username\" id=\"username\" type=\"text\" required=\"required\" title=\"Username or Email\" placeholder=\"Enter your username or email\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"username\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validateusername(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validateusername(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('username_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'username_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validateusername(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('username_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-group\"\u003e\u003clabel for=\"password\"\u003ePassword \u003cspan class=\"required\"\u003e*\u003c/span\u003e\u003c/label\u003e\u003cinput name=\"password\" id=\"password\" type=\"password\" required=\"required\" title=\"Password\" placeholder=\"Enter your password\" class=\"form-group\" /\u003e\n\t\t\u003cscript\u003e\n\t\t(function() {\n\t\t\tvar field = document.querySelector('[name=\"password\"]');\n\t\t\tif (!field) return;\n\n\t\t\tfunction validatepassword(value) {\n\t\t\t\t\n\t\t\tif (!value || (typeof value === 'string' \u0026\u0026 value.trim() === '')) {\n\t\t\t\treturn 'This field is required';\n\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfunction validateForm() {\n\t\t\t\t// Dependent validations that check other fields\n\t\t\t\t\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfield.addEventListener('blur', function() {\n\t\t\t\tvar error = validatepassword(this.value) || validateForm();\n\t\t\t\tvar errorElement = document.getElementById('password_error');\n\n\t\t\t\tif (error) {\n\t\t\t\t\tif (!errorElement) {\n\t\t\t\t\t\terrorElement = document.createElement('div');\n\t\t\t\t\t\terrorElement.id = 'password_error';\n\t\t\t\t\t\terrorElement.className = 'validation-error';\n\t\t\t\t\t\terrorElement.style.color = 'red';\n\t\t\t\t\t\terrorElement.style.fontSize = '0.875rem';\n\t\t\t\t\t\terrorElement.style.marginTop = '0.25rem';\n\t\t\t\t\t\tthis.parentNode.appendChild(errorElement);\n\t\t\t\t\t}\n\t\t\t\t\terrorElement.textContent = error;\n\t\t\t\t\tthis.classList.add('invalid');\n\t\t\t\t\tthis.style.borderColor = 'red';\n\t\t\t\t} else {\n\t\t\t\t\tif (errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t}\n\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Also validate on input for immediate feedback\n\t\t\tfield.addEventListener('input', function() {\n\t\t\t\tif (this.classList.contains('invalid')) {\n\t\t\t\t\tvar error = validatepassword(this.value);\n\t\t\t\t\tvar errorElement = document.getElementById('password_error');\n\t\t\t\t\tif (!error \u0026\u0026 errorElement) {\n\t\t\t\t\t\terrorElement.remove();\n\t\t\t\t\t\tthis.classList.remove('invalid');\n\t\t\t\t\t\tthis.style.borderColor = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t\t\u003c/script\u003e\n\t\u003c/div\u003e\u003cdiv class=\"form-check\"\u003e\u003clabel for=\"remember_me\"\u003eRemember Me\u003c/label\u003e\u003cinput name=\"remember_me\" id=\"remember_me\" type=\"checkbox\" title=\"Remember Me\" placeholder=\"Enter remember me\" class=\"form-check\" /\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n \u003cdiv class=\"mt-4 flex gap-2\"\u003e\n \u003cbutton type=\"submit\" class=\"btn btn-primary\"\u003eLog In\u003c/button\u003e\u003cbutton type=\"reset\" class=\"btn btn-secondary\"\u003eClear\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/form\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n","login_message":"Login successful!","next":"true","password":"password","step":"form","task_id":"345181241327013889","username":"admin"} diff --git a/services/examples/app/templates/basic.html b/services/examples/app/templates/basic.html deleted file mode 100644 index f4ed557..0000000 --- a/services/examples/app/templates/basic.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Basic Template - - - - - - -
-
- {{form_groups}} -
- {{form_buttons}} -
-
-
- - - diff --git a/services/examples/app/templates/error.html b/services/examples/app/templates/error.html deleted file mode 100644 index 9935457..0000000 --- a/services/examples/app/templates/error.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - Email Error - - - - -
-
-

Email Processing Error

- -
- {{error_message}} -
- - {{if error_field}} -
- 🎯 Error Field: {{error_field}}
- ⚡ Action Required: Please correct the highlighted field and try again.
- 💡 Tip: Make sure all required fields are properly filled out. -
- {{end}} - - {{if retry_suggested}} -
- ⚠️ Temporary Issue: This appears to be a temporary system issue. - Please try sending your message again in a few moments.
- 🔄 Auto-Retry: Our system will automatically retry failed deliveries. -
- {{end}} - -
- 🔄 Try Again - 📊 Check Status -
- -
- 🔄 DAG Error Handler | Email Notification Workflow Failed
- Our advanced routing system ensures reliable message delivery. -
-
- - - diff --git a/services/examples/config/policies/apis/sample.json b/services/examples/config/policies/apis/sample.json deleted file mode 100644 index 9f0df17..0000000 --- a/services/examples/config/policies/apis/sample.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "routes": [ - { - "route_uri": "/test-route", - "route_method": "POST", - "schema_file": "test-route.json", - "description": "Handle test route", - "model": "test_route", - "operation": "custom", - "handler_key": "print:check" - }, - { - "route_uri": "/print", - "route_method": "GET", - "description": "Handles print", - "model": "print", - "operation": "custom", - "handler_key": "print:check" - } - ] -} diff --git a/services/examples/config/policies/handlers/login.json b/services/examples/config/policies/handlers/login.json deleted file mode 100644 index 06bf793..0000000 --- a/services/examples/config/policies/handlers/login.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "Login Flow", - "key": "login:flow", - "nodes": [ - { - "id": "LoginForm", - "first_node": true, - "node": "render-html", - "data": { - "additional_data": { - "schema_file": "login.json", - "template_file": "app/templates/basic.html" - } - } - }, - { - "id": "ValidateLogin", - "node": "condition", - "data": { - "mapping": { - "username": "username", - "password": "password" - }, - "additional_data": { - "conditions": { - "default": { - "id": "condition:default", - "node": "output" - }, - "invalid": { - "id": "condition:invalid_login", - "node": "error-page", - "group": { - "reverse": true, - "filters": [ - { - "field": "username", - "operator": "eq", - "value": "admin" - }, - { - "field": "password", - "operator": "eq", - "value": "password" - } - ] - } - } - } - } - } - }, - { - "id": "error-page", - "node": "render-html", - "data": { - "mapping": { - "error_message": "eval.{{'Invalid login credentials.'}}", - "error_field": "eval.{{'username'}}", - "retry_suggested": "eval.{{true}}" - }, - "additional_data": { - "template_file": "app/templates/error.html" - } - } - }, - { - "id": "output", - "node": "output", - "data": { - "additional_data": { - "output_type": "file", - "file_path": "app/data/login_output.json" - }, - "mapping": { - "login_message": "eval.{{'Login successful!'}}" - } - } - } - ], - "edges": [ - { - "source": "LoginForm", - "target": [ "ValidateLogin" ] - } - ] - -} diff --git a/services/examples/config/policies/handlers/print-check.json b/services/examples/config/policies/handlers/print-check.json deleted file mode 100644 index 41c8ebd..0000000 --- a/services/examples/config/policies/handlers/print-check.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Sample Print", - "key": "print:check", - "nodes": [ - { - "id": "print1", - "node": "print", - "first_node": true - } - ] -} diff --git a/services/examples/config/policies/handlers/send-email.json b/services/examples/config/policies/handlers/send-email.json deleted file mode 100644 index 1c5d19c..0000000 --- a/services/examples/config/policies/handlers/send-email.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "Email Notification System", - "key": "email:notification", - "nodes": [ - { - "id": "Login", - "name": "Check Login", - "node_key": "login:flow", - "first_node": true - }, - { - "id": "ContactForm", - "node": "render-html", - "data": { - "additional_data": { - "schema_file": "schema.json", - "template_file": "app/templates/basic.html" - } - } - } - ], - "edges": [ - { - "source": "Login.output", - "label": "on_success", - "target": [ "ContactForm" ] - } - ] -} diff --git a/services/examples/config/policies/schemas/login.json b/services/examples/config/policies/schemas/login.json deleted file mode 100644 index 032714a..0000000 --- a/services/examples/config/policies/schemas/login.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "type": "object", - "properties": { - "username": { - "type": "string", - "title": "Username or Email", - "order": 1, - "ui": { - "element": "input", - "type": "text", - "class": "form-group", - "name": "username", - "placeholder": "Enter your username or email" - } - }, - "password": { - "type": "string", - "title": "Password", - "order": 2, - "ui": { - "element": "input", - "type": "password", - "class": "form-group", - "name": "password", - "placeholder": "Enter your password" - } - }, - "remember_me": { - "type": "boolean", - "title": "Remember Me", - "order": 3, - "ui": { - "element": "input", - "type": "checkbox", - "class": "form-check", - "name": "remember_me" - } - } - }, - "required": [ "username", "password" ], - "form": { - "class": "form-horizontal", - "action": "{{current_uri}}?task_id={{task_id}}&next=true", - "method": "POST", - "enctype": "application/x-www-form-urlencoded", - "groups": [ - { - "title": "Login Credentials", - "fields": [ "username", "password", "remember_me" ] - } - ], - "submit": { - "type": "submit", - "label": "Log In", - "class": "btn btn-primary" - }, - "reset": { - "type": "reset", - "label": "Clear", - "class": "btn btn-secondary" - } - } -} diff --git a/services/examples/config/policies/schemas/schema.json b/services/examples/config/policies/schemas/schema.json deleted file mode 100644 index 02dd052..0000000 --- a/services/examples/config/policies/schemas/schema.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "type": "object", - "properties": { - "first_name": { - "type": "string", - "title": "First Name", - "order": 1, - "ui": { - "element": "input", - "class": "form-group", - "name": "first_name" - } - }, - "last_name": { - "type": "string", - "title": "Last Name", - "order": 2, - "ui": { - "element": "input", - "class": "form-group", - "name": "last_name" - } - }, - "email": { - "type": "email", - "title": "Email Address", - "order": 3, - "ui": { - "element": "input", - "type": "email", - "class": "form-group", - "name": "email" - } - }, - "user_type": { - "type": "string", - "title": "User Type", - "order": 4, - "ui": { - "element": "select", - "class": "form-group", - "name": "user_type", - "options": [ "new", "premium", "standard" ] - } - }, - "priority": { - "type": "string", - "title": "Priority Level", - "order": 5, - "ui": { - "element": "select", - "class": "form-group", - "name": "priority", - "options": [ "low", "medium", "high", "urgent" ] - } - }, - "subject": { - "type": "string", - "title": "Subject", - "order": 6, - "ui": { - "element": "input", - "class": "form-group", - "name": "subject" - } - }, - "message": { - "type": "textarea", - "title": "Message", - "order": 7, - "ui": { - "element": "textarea", - "class": "form-group", - "name": "message" - } - } - }, - "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ], - "form": { - "class": "form-horizontal", - "action": "/process?task_id={{task_id}}&next=true", - "method": "POST", - "enctype": "application/x-www-form-urlencoded", - "groups": [ - { - "title": "User Information", - "fields": [ "first_name", "last_name", "email" ] - }, - { - "title": "Ticket Details", - "fields": [ "user_type", "priority", "subject", "message" ] - } - ], - "submit": { - "type": "submit", - "label": "Submit", - "class": "btn btn-primary" - }, - "reset": { - "type": "reset", - "label": "Reset", - "class": "btn btn-secondary" - } - } -} diff --git a/services/examples/config/policies/schemas/test-route.json b/services/examples/config/policies/schemas/test-route.json deleted file mode 100644 index 076ba1c..0000000 --- a/services/examples/config/policies/schemas/test-route.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "description": "users", - "required": [ "user_id" ], - "properties": { - "last_name": { - "type": "string", - "default": "now()" - }, - "user_id": { - "type": [ - "integer", - "string" - ], - "maxLength": 64 - } - } -} \ No newline at end of file diff --git a/services/examples/config/policies/web.json b/services/examples/config/policies/web.json deleted file mode 100644 index 0c83df7..0000000 --- a/services/examples/config/policies/web.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "prefix": "/", - "middlewares": [ - {"name": "cors"} - ], - "static": { - "dir": "./public", - "prefix": "/", - "options": { - "byte_range": true, - "browse": true, - "compress": true, - "index_file": "index.html" - } - } -} diff --git a/services/examples/main.go b/services/examples/main.go deleted file mode 100644 index 3893424..0000000 --- a/services/examples/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "github.com/gofiber/fiber/v2" - "github.com/oarkflow/cli" - "github.com/oarkflow/cli/console" - "github.com/oarkflow/cli/contracts" - - "github.com/oarkflow/mq" - "github.com/oarkflow/mq/dag" - "github.com/oarkflow/mq/handlers" - "github.com/oarkflow/mq/services" - dagConsole "github.com/oarkflow/mq/services/console" -) - -func main() { - handlers.Init() - brokerAddr := ":5051" - loader := services.NewLoader("config") - loader.Load() - serverApp := fiber.New(fiber.Config{EnablePrintRoutes: true}) - services.Setup(loader, serverApp, brokerAddr) - cli.Run("mq", "0.0.1", func(client contracts.Cli) []contracts.Command { - return []contracts.Command{ - console.NewListCommand(client), - dagConsole.NewRunHandler(loader.UserConfig, loader.ParsedPath, brokerAddr), - dagConsole.NewRunServer(serverApp), - } - }) -} - -func init() { - dag.AddHandler("render-html", func(id string) mq.Processor { return handlers.NewRenderHTMLNode(id) }) - dag.AddHandler("condition", func(id string) mq.Processor { return handlers.NewCondition(id) }) - dag.AddHandler("output", func(id string) mq.Processor { return handlers.NewOutputHandler(id) }) -} diff --git a/services/examples/public/assets/InterVariable-CWi-zmRD.woff2 b/services/examples/public/assets/InterVariable-CWi-zmRD.woff2 deleted file mode 100644 index 22a12b0..0000000 Binary files a/services/examples/public/assets/InterVariable-CWi-zmRD.woff2 and /dev/null differ diff --git a/services/examples/public/assets/InterVariable-Italic-d6KXgdvN.woff2 b/services/examples/public/assets/InterVariable-Italic-d6KXgdvN.woff2 deleted file mode 100644 index f22ec25..0000000 Binary files a/services/examples/public/assets/InterVariable-Italic-d6KXgdvN.woff2 and /dev/null differ diff --git a/services/examples/public/assets/index-B0w5Q249.js b/services/examples/public/assets/index-B0w5Q249.js deleted file mode 100644 index 4bdd481..0000000 --- a/services/examples/public/assets/index-B0w5Q249.js +++ /dev/null @@ -1,1102 +0,0 @@ -(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))l(r);new MutationObserver(r=>{for(const d of r)if(d.type==="childList")for(const h of d.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&l(h)}).observe(document,{childList:!0,subtree:!0});function t(r){const d={};return r.integrity&&(d.integrity=r.integrity),r.referrerPolicy&&(d.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?d.credentials="include":r.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function l(r){if(r.ep)return;r.ep=!0;const d=t(r);fetch(r.href,d)}})();function Um(a){return a&&a.__esModule&&Object.prototype.hasOwnProperty.call(a,"default")?a.default:a}var af={exports:{}},il={},of={exports:{}},gt={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Vp;function Lh(){if(Vp)return gt;Vp=1;var a=Symbol.for("react.element"),i=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),l=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),d=Symbol.for("react.provider"),h=Symbol.for("react.context"),v=Symbol.for("react.forward_ref"),g=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),k=Symbol.for("react.lazy"),E=Symbol.iterator;function N(L){return L===null||typeof L!="object"?null:(L=E&&L[E]||L["@@iterator"],typeof L=="function"?L:null)}var I={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},M=Object.assign,D={};function B(L,X,K){this.props=L,this.context=X,this.refs=D,this.updater=K||I}B.prototype.isReactComponent={},B.prototype.setState=function(L,X){if(typeof L!="object"&&typeof L!="function"&&L!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,L,X,"setState")},B.prototype.forceUpdate=function(L){this.updater.enqueueForceUpdate(this,L,"forceUpdate")};function _(){}_.prototype=B.prototype;function $(L,X,K){this.props=L,this.context=X,this.refs=D,this.updater=K||I}var W=$.prototype=new _;W.constructor=$,M(W,B.prototype),W.isPureReactComponent=!0;var he=Array.isArray,Se=Object.prototype.hasOwnProperty,Oe={current:null},ye={key:!0,ref:!0,__self:!0,__source:!0};function De(L,X,K){var J,ie={},ve=null,Ne=null;if(X!=null)for(J in X.ref!==void 0&&(Ne=X.ref),X.key!==void 0&&(ve=""+X.key),X)Se.call(X,J)&&!ye.hasOwnProperty(J)&&(ie[J]=X[J]);var qe=arguments.length-2;if(qe===1)ie.children=K;else if(1>>1,X=te[L];if(0>>1;Lr(ie,we))ver(Ne,ie)?(te[L]=Ne,te[ve]=we,L=ve):(te[L]=ie,te[J]=we,L=J);else if(ver(Ne,we))te[L]=Ne,te[ve]=we,L=ve;else break e}}return de}function r(te,de){var we=te.sortIndex-de.sortIndex;return we!==0?we:te.id-de.id}if(typeof performance=="object"&&typeof performance.now=="function"){var d=performance;a.unstable_now=function(){return d.now()}}else{var h=Date,v=h.now();a.unstable_now=function(){return h.now()-v}}var g=[],y=[],k=1,E=null,N=3,I=!1,M=!1,D=!1,B=typeof setTimeout=="function"?setTimeout:null,_=typeof clearTimeout=="function"?clearTimeout:null,$=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function W(te){for(var de=t(y);de!==null;){if(de.callback===null)l(y);else if(de.startTime<=te)l(y),de.sortIndex=de.expirationTime,i(g,de);else break;de=t(y)}}function he(te){if(D=!1,W(te),!M)if(t(g)!==null)M=!0,Ye(Se);else{var de=t(y);de!==null&&Ve(he,de.startTime-te)}}function Se(te,de){M=!1,D&&(D=!1,_(De),De=-1),I=!0;var we=N;try{for(W(de),E=t(g);E!==null&&(!(E.expirationTime>de)||te&&!oe());){var L=E.callback;if(typeof L=="function"){E.callback=null,N=E.priorityLevel;var X=L(E.expirationTime<=de);de=a.unstable_now(),typeof X=="function"?E.callback=X:E===t(g)&&l(g),W(de)}else l(g);E=t(g)}if(E!==null)var K=!0;else{var J=t(y);J!==null&&Ve(he,J.startTime-de),K=!1}return K}finally{E=null,N=we,I=!1}}var Oe=!1,ye=null,De=-1,z=5,U=-1;function oe(){return!(a.unstable_now()-Ute||125L?(te.sortIndex=we,i(y,te),t(g)===null&&te===t(y)&&(D?(_(De),De=-1):D=!0,Ve(he,we-L))):(te.sortIndex=X,i(g,te),M||I||(M=!0,Ye(Se))),te},a.unstable_shouldYield=oe,a.unstable_wrapCallback=function(te){var de=N;return function(){var we=N;N=de;try{return te.apply(this,arguments)}finally{N=we}}}}(sf)),sf}var Gp;function $h(){return Gp||(Gp=1,uf.exports=_h()),uf.exports}/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Qp;function Bh(){if(Qp)return Pr;Qp=1;var a=Wf(),i=$h();function t(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,o=1;o"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),g=Object.prototype.hasOwnProperty,y=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,k={},E={};function N(e){return g.call(E,e)?!0:g.call(k,e)?!1:y.test(e)?E[e]=!0:(k[e]=!0,!1)}function I(e,n,o,u){if(o!==null&&o.type===0)return!1;switch(typeof n){case"function":case"symbol":return!0;case"boolean":return u?!1:o!==null?!o.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function M(e,n,o,u){if(n===null||typeof n>"u"||I(e,n,o,u))return!0;if(u)return!1;if(o!==null)switch(o.type){case 3:return!n;case 4:return n===!1;case 5:return isNaN(n);case 6:return isNaN(n)||1>n}return!1}function D(e,n,o,u,s,f,w){this.acceptsBooleans=n===2||n===3||n===4,this.attributeName=u,this.attributeNamespace=s,this.mustUseProperty=o,this.propertyName=e,this.type=n,this.sanitizeURL=f,this.removeEmptyString=w}var B={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){B[e]=new D(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var n=e[0];B[n]=new D(n,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){B[e]=new D(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){B[e]=new D(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){B[e]=new D(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){B[e]=new D(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){B[e]=new D(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){B[e]=new D(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){B[e]=new D(e,5,!1,e.toLowerCase(),null,!1,!1)});var _=/[\-:]([a-z])/g;function $(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var n=e.replace(_,$);B[n]=new D(n,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var n=e.replace(_,$);B[n]=new D(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var n=e.replace(_,$);B[n]=new D(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){B[e]=new D(e,1,!1,e.toLowerCase(),null,!1,!1)}),B.xlinkHref=new D("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){B[e]=new D(e,1,!1,e.toLowerCase(),null,!0,!0)});function W(e,n,o,u){var s=B.hasOwnProperty(n)?B[n]:null;(s!==null?s.type!==0:u||!(2O||s[w]!==f[O]){var T=` -`+s[w].replace(" at new "," at ");return e.displayName&&T.includes("")&&(T=T.replace("",e.displayName)),T}while(1<=w&&0<=O);break}}}finally{K=!1,Error.prepareStackTrace=o}return(e=e?e.displayName||e.name:"")?X(e):""}function ie(e){switch(e.tag){case 5:return X(e.type);case 16:return X("Lazy");case 13:return X("Suspense");case 19:return X("SuspenseList");case 0:case 2:case 15:return e=J(e.type,!1),e;case 11:return e=J(e.type.render,!1),e;case 1:return e=J(e.type,!0),e;default:return""}}function ve(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case ye:return"Fragment";case Oe:return"Portal";case z:return"Profiler";case De:return"StrictMode";case q:return"Suspense";case Ie:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case oe:return(e.displayName||"Context")+".Consumer";case U:return(e._context.displayName||"Context")+".Provider";case _e:var n=e.render;return e=e.displayName,e||(e=n.displayName||n.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case se:return n=e.displayName||null,n!==null?n:ve(e.type)||"Memo";case Ye:n=e._payload,e=e._init;try{return ve(e(n))}catch{}}return null}function Ne(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=n.render,e=e.displayName||e.name||"",n.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ve(n);case 8:return n===De?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n}return null}function qe(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function dt(e){var n=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function Ue(e){var n=dt(e)?"checked":"value",o=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),u=""+e[n];if(!e.hasOwnProperty(n)&&typeof o<"u"&&typeof o.get=="function"&&typeof o.set=="function"){var s=o.get,f=o.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return s.call(this)},set:function(w){u=""+w,f.call(this,w)}}),Object.defineProperty(e,n,{enumerable:o.enumerable}),{getValue:function(){return u},setValue:function(w){u=""+w},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}function Bt(e){e._valueTracker||(e._valueTracker=Ue(e))}function Yt(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var o=n.getValue(),u="";return e&&(u=dt(e)?e.checked?"true":"false":e.value),e=u,e!==o?(n.setValue(e),!0):!1}function Pt(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function yt(e,n){var o=n.checked;return we({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:o??e._wrapperState.initialChecked})}function zt(e,n){var o=n.defaultValue==null?"":n.defaultValue,u=n.checked!=null?n.checked:n.defaultChecked;o=qe(n.value!=null?n.value:o),e._wrapperState={initialChecked:u,initialValue:o,controlled:n.type==="checkbox"||n.type==="radio"?n.checked!=null:n.value!=null}}function dr(e,n){n=n.checked,n!=null&&W(e,"checked",n,!1)}function rn(e,n){dr(e,n);var o=qe(n.value),u=n.type;if(o!=null)u==="number"?(o===0&&e.value===""||e.value!=o)&&(e.value=""+o):e.value!==""+o&&(e.value=""+o);else if(u==="submit"||u==="reset"){e.removeAttribute("value");return}n.hasOwnProperty("value")?dn(e,n.type,o):n.hasOwnProperty("defaultValue")&&dn(e,n.type,qe(n.defaultValue)),n.checked==null&&n.defaultChecked!=null&&(e.defaultChecked=!!n.defaultChecked)}function Dt(e,n,o){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var u=n.type;if(!(u!=="submit"&&u!=="reset"||n.value!==void 0&&n.value!==null))return;n=""+e._wrapperState.initialValue,o||n===e.value||(e.value=n),e.defaultValue=n}o=e.name,o!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,o!==""&&(e.name=o)}function dn(e,n,o){(n!=="number"||Pt(e.ownerDocument)!==e)&&(o==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+o&&(e.defaultValue=""+o))}var In=Array.isArray;function Ht(e,n,o,u){if(e=e.options,n){n={};for(var s=0;s"+n.valueOf().toString()+"",n=Tn.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}});function bn(e,n){if(n){var o=e.firstChild;if(o&&o===e.lastChild&&o.nodeType===3){o.nodeValue=n;return}}e.textContent=n}var Gt={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},qn=["Webkit","ms","Moz","O"];Object.keys(Gt).forEach(function(e){qn.forEach(function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),Gt[n]=Gt[e]})});function mn(e,n,o){return n==null||typeof n=="boolean"||n===""?"":o||typeof n!="number"||n===0||Gt.hasOwnProperty(e)&&Gt[e]?(""+n).trim():n+"px"}function zn(e,n){e=e.style;for(var o in n)if(n.hasOwnProperty(o)){var u=o.indexOf("--")===0,s=mn(o,n[o],u);o==="float"&&(o="cssFloat"),u?e.setProperty(o,s):e[o]=s}}var Jn=we({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function vr(e,n){if(n){if(Jn[e]&&(n.children!=null||n.dangerouslySetInnerHTML!=null))throw Error(t(137,e));if(n.dangerouslySetInnerHTML!=null){if(n.children!=null)throw Error(t(60));if(typeof n.dangerouslySetInnerHTML!="object"||!("__html"in n.dangerouslySetInnerHTML))throw Error(t(61))}if(n.style!=null&&typeof n.style!="object")throw Error(t(62))}}function er(e,n){if(e.indexOf("-")===-1)return typeof n.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var jn=null;function Ke(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ne=null,Me=null,at=null;function lt(e){if(e=Uo(e)){if(typeof ne!="function")throw Error(t(280));var n=e.stateNode;n&&(n=ru(n),ne(e.stateNode,e.type,n))}}function Je(e){Me?at?at.push(e):at=[e]:Me=e}function ft(){if(Me){var e=Me,n=at;if(at=Me=null,lt(e),n)for(e=0;e>>=0,e===0?32:31-(it(e)/Vt|0)|0}var ht=64,Nt=4194304;function ct(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Xe(e,n){var o=e.pendingLanes;if(o===0)return 0;var u=0,s=e.suspendedLanes,f=e.pingedLanes,w=o&268435455;if(w!==0){var O=w&~s;O!==0?u=ct(O):(f&=w,f!==0&&(u=ct(f)))}else w=o&~s,w!==0?u=ct(w):f!==0&&(u=ct(f));if(u===0)return 0;if(n!==0&&n!==u&&!(n&s)&&(s=u&-u,f=n&-n,s>=f||s===16&&(f&4194240)!==0))return n;if(u&4&&(u|=o&16),n=e.entangledLanes,n!==0)for(e=e.entanglements,n&=u;0o;o++)n.push(e);return n}function qt(e,n,o){e.pendingLanes|=n,n!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,n=31-Fe(n),e[n]=o}function Qt(e,n){var o=e.pendingLanes&~n;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=n,e.mutableReadLanes&=n,e.entangledLanes&=n,n=e.entanglements;var u=e.eventTimes;for(e=e.expirationTimes;0=bi),_o=" ",Wa=!1;function Ya(e,n){switch(e){case"keyup":return Fs.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Yl(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ga=!1;function _s(e,n){switch(e){case"compositionend":return Yl(n);case"keypress":return n.which!==32?null:(Wa=!0,_o);case"textInput":return e=n.data,e===_o&&Wa?null:e;default:return null}}function $s(e,n){if(Ga)return e==="compositionend"||!Sa&&Ya(e,n)?(e=jo(),Ha=Ki=Kn=null,Ga=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:o,offset:n-e};e=u}e:{for(;o;){if(o.nextSibling){o=o.nextSibling;break e}o=o.parentNode}o=void 0}o=c(o)}}function x(e,n){return e&&n?e===n?!0:e&&e.nodeType===3?!1:n&&n.nodeType===3?x(e,n.parentNode):"contains"in e?e.contains(n):e.compareDocumentPosition?!!(e.compareDocumentPosition(n)&16):!1:!1}function b(){for(var e=window,n=Pt();n instanceof e.HTMLIFrameElement;){try{var o=typeof n.contentWindow.location.href=="string"}catch{o=!1}if(o)e=n.contentWindow;else break;n=Pt(e.document)}return n}function F(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&(n==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||n==="textarea"||e.contentEditable==="true")}function V(e){var n=b(),o=e.focusedElem,u=e.selectionRange;if(n!==o&&o&&o.ownerDocument&&x(o.ownerDocument.documentElement,o)){if(u!==null&&F(o)){if(n=u.start,e=u.end,e===void 0&&(e=n),"selectionStart"in o)o.selectionStart=n,o.selectionEnd=Math.min(e,o.value.length);else if(e=(n=o.ownerDocument||document)&&n.defaultView||window,e.getSelection){e=e.getSelection();var s=o.textContent.length,f=Math.min(u.start,s);u=u.end===void 0?f:Math.min(u.end,s),!e.extend&&f>u&&(s=u,u=f,f=s),s=p(o,f);var w=p(o,u);s&&w&&(e.rangeCount!==1||e.anchorNode!==s.node||e.anchorOffset!==s.offset||e.focusNode!==w.node||e.focusOffset!==w.offset)&&(n=n.createRange(),n.setStart(s.node,s.offset),e.removeAllRanges(),f>u?(e.addRange(n),e.extend(w.node,w.offset)):(n.setEnd(w.node,w.offset),e.addRange(n)))}}for(n=[],e=o;e=e.parentNode;)e.nodeType===1&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof o.focus=="function"&&o.focus(),o=0;o=document.documentMode,ae=null,Le=null,rt=null,ut=!1;function Te(e,n,o){var u=o.window===o?o.document:o.nodeType===9?o:o.ownerDocument;ut||ae==null||ae!==Pt(u)||(u=ae,"selectionStart"in u&&F(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),rt&&R(rt,u)||(rt=u,u=eu(Le,"onSelect"),0Ji||(e.current=tc[Ji],tc[Ji]=null,Ji--)}function $t(e,n){Ji++,tc[Ji]=e.current,e.current=n}var Za={},or=Xa(Za),br=Xa(!1),Oi=Za;function eo(e,n){var o=e.type.contextTypes;if(!o)return Za;var u=e.stateNode;if(u&&u.__reactInternalMemoizedUnmaskedChildContext===n)return u.__reactInternalMemoizedMaskedChildContext;var s={},f;for(f in o)s[f]=n[f];return u&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=s),s}function Sr(e){return e=e.childContextTypes,e!=null}function au(){Wt(br),Wt(or)}function fd(e,n,o){if(or.current!==Za)throw Error(t(168));$t(or,n),$t(br,o)}function dd(e,n,o){var u=e.stateNode;if(n=n.childContextTypes,typeof u.getChildContext!="function")return o;u=u.getChildContext();for(var s in u)if(!(s in n))throw Error(t(108,Ne(e)||"Unknown",s));return we({},o,u)}function iu(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Za,Oi=or.current,$t(or,e),$t(br,br.current),!0}function pd(e,n,o){var u=e.stateNode;if(!u)throw Error(t(169));o?(e=dd(e,n,Oi),u.__reactInternalMemoizedMergedChildContext=e,Wt(br),Wt(or),$t(or,e)):Wt(br),$t(br,o)}var Pa=null,ou=!1,nc=!1;function md(e){Pa===null?Pa=[e]:Pa.push(e)}function th(e){ou=!0,md(e)}function qa(){if(!nc&&Pa!==null){nc=!0;var e=0,n=vt;try{var o=Pa;for(vt=1;e>=w,s-=w,Oa=1<<32-Fe(n)+s|o<st?(An=nt,nt=null):An=nt.sibling;var kt=me(H,nt,Y[st],Ce);if(kt===null){nt===null&&(nt=An);break}e&&nt&&kt.alternate===null&&n(H,nt),j=f(kt,j,st),tt===null?He=kt:tt.sibling=kt,tt=kt,nt=An}if(st===Y.length)return o(H,nt),Xt&&Ni(H,st),He;if(nt===null){for(;stst?(An=nt,nt=null):An=nt.sibling;var li=me(H,nt,kt.value,Ce);if(li===null){nt===null&&(nt=An);break}e&&nt&&li.alternate===null&&n(H,nt),j=f(li,j,st),tt===null?He=li:tt.sibling=li,tt=li,nt=An}if(kt.done)return o(H,nt),Xt&&Ni(H,st),He;if(nt===null){for(;!kt.done;st++,kt=Y.next())kt=be(H,kt.value,Ce),kt!==null&&(j=f(kt,j,st),tt===null?He=kt:tt.sibling=kt,tt=kt);return Xt&&Ni(H,st),He}for(nt=u(H,nt);!kt.done;st++,kt=Y.next())kt=Re(nt,H,st,kt.value,Ce),kt!==null&&(e&&kt.alternate!==null&&nt.delete(kt.key===null?st:kt.key),j=f(kt,j,st),tt===null?He=kt:tt.sibling=kt,tt=kt);return e&&nt.forEach(function(Rh){return n(H,Rh)}),Xt&&Ni(H,st),He}function fn(H,j,Y,Ce){if(typeof Y=="object"&&Y!==null&&Y.type===ye&&Y.key===null&&(Y=Y.props.children),typeof Y=="object"&&Y!==null){switch(Y.$$typeof){case Se:e:{for(var He=Y.key,tt=j;tt!==null;){if(tt.key===He){if(He=Y.type,He===ye){if(tt.tag===7){o(H,tt.sibling),j=s(tt,Y.props.children),j.return=H,H=j;break e}}else if(tt.elementType===He||typeof He=="object"&&He!==null&&He.$$typeof===Ye&&xd(He)===tt.type){o(H,tt.sibling),j=s(tt,Y.props),j.ref=Ko(H,tt,Y),j.return=H,H=j;break e}o(H,tt);break}else n(H,tt);tt=tt.sibling}Y.type===ye?(j=Ai(Y.props.children,H.mode,Ce,Y.key),j.return=H,H=j):(Ce=Ru(Y.type,Y.key,Y.props,null,H.mode,Ce),Ce.ref=Ko(H,j,Y),Ce.return=H,H=Ce)}return w(H);case Oe:e:{for(tt=Y.key;j!==null;){if(j.key===tt)if(j.tag===4&&j.stateNode.containerInfo===Y.containerInfo&&j.stateNode.implementation===Y.implementation){o(H,j.sibling),j=s(j,Y.children||[]),j.return=H,H=j;break e}else{o(H,j);break}else n(H,j);j=j.sibling}j=Jc(Y,H.mode,Ce),j.return=H,H=j}return w(H);case Ye:return tt=Y._init,fn(H,j,tt(Y._payload),Ce)}if(In(Y))return $e(H,j,Y,Ce);if(de(Y))return ze(H,j,Y,Ce);cu(H,Y)}return typeof Y=="string"&&Y!==""||typeof Y=="number"?(Y=""+Y,j!==null&&j.tag===6?(o(H,j.sibling),j=s(j,Y),j.return=H,H=j):(o(H,j),j=qc(Y,H.mode,Ce),j.return=H,H=j),w(H)):o(H,j)}return fn}var ao=bd(!0),Sd=bd(!1),fu=Xa(null),du=null,io=null,uc=null;function sc(){uc=io=du=null}function cc(e){var n=fu.current;Wt(fu),e._currentValue=n}function fc(e,n,o){for(;e!==null;){var u=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,u!==null&&(u.childLanes|=n)):u!==null&&(u.childLanes&n)!==n&&(u.childLanes|=n),e===o)break;e=e.return}}function oo(e,n){du=e,uc=io=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&n&&(Er=!0),e.firstContext=null)}function Vr(e){var n=e._currentValue;if(uc!==e)if(e={context:e,memoizedValue:n,next:null},io===null){if(du===null)throw Error(t(308));io=e,du.dependencies={lanes:0,firstContext:e}}else io=io.next=e;return n}var Ii=null;function dc(e){Ii===null?Ii=[e]:Ii.push(e)}function Ed(e,n,o,u){var s=n.interleaved;return s===null?(o.next=o,dc(n)):(o.next=s.next,s.next=o),n.interleaved=o,Na(e,u)}function Na(e,n){e.lanes|=n;var o=e.alternate;for(o!==null&&(o.lanes|=n),o=e,e=e.return;e!==null;)e.childLanes|=n,o=e.alternate,o!==null&&(o.childLanes|=n),o=e,e=e.return;return o.tag===3?o.stateNode:null}var Ja=!1;function pc(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Cd(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ia(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function ei(e,n,o){var u=e.updateQueue;if(u===null)return null;if(u=u.shared,Et&2){var s=u.pending;return s===null?n.next=n:(n.next=s.next,s.next=n),u.pending=n,Na(e,o)}return s=u.interleaved,s===null?(n.next=n,dc(u)):(n.next=s.next,s.next=n),u.interleaved=n,Na(e,o)}function pu(e,n,o){if(n=n.updateQueue,n!==null&&(n=n.shared,(o&4194240)!==0)){var u=n.lanes;u&=e.pendingLanes,o|=u,n.lanes=o,ar(e,o)}}function kd(e,n){var o=e.updateQueue,u=e.alternate;if(u!==null&&(u=u.updateQueue,o===u)){var s=null,f=null;if(o=o.firstBaseUpdate,o!==null){do{var w={eventTime:o.eventTime,lane:o.lane,tag:o.tag,payload:o.payload,callback:o.callback,next:null};f===null?s=f=w:f=f.next=w,o=o.next}while(o!==null);f===null?s=f=n:f=f.next=n}else s=f=n;o={baseState:u.baseState,firstBaseUpdate:s,lastBaseUpdate:f,shared:u.shared,effects:u.effects},e.updateQueue=o;return}e=o.lastBaseUpdate,e===null?o.firstBaseUpdate=n:e.next=n,o.lastBaseUpdate=n}function mu(e,n,o,u){var s=e.updateQueue;Ja=!1;var f=s.firstBaseUpdate,w=s.lastBaseUpdate,O=s.shared.pending;if(O!==null){s.shared.pending=null;var T=O,Q=T.next;T.next=null,w===null?f=Q:w.next=Q,w=T;var ge=e.alternate;ge!==null&&(ge=ge.updateQueue,O=ge.lastBaseUpdate,O!==w&&(O===null?ge.firstBaseUpdate=Q:O.next=Q,ge.lastBaseUpdate=T))}if(f!==null){var be=s.baseState;w=0,ge=Q=T=null,O=f;do{var me=O.lane,Re=O.eventTime;if((u&me)===me){ge!==null&&(ge=ge.next={eventTime:Re,lane:0,tag:O.tag,payload:O.payload,callback:O.callback,next:null});e:{var $e=e,ze=O;switch(me=n,Re=o,ze.tag){case 1:if($e=ze.payload,typeof $e=="function"){be=$e.call(Re,be,me);break e}be=$e;break e;case 3:$e.flags=$e.flags&-65537|128;case 0:if($e=ze.payload,me=typeof $e=="function"?$e.call(Re,be,me):$e,me==null)break e;be=we({},be,me);break e;case 2:Ja=!0}}O.callback!==null&&O.lane!==0&&(e.flags|=64,me=s.effects,me===null?s.effects=[O]:me.push(O))}else Re={eventTime:Re,lane:me,tag:O.tag,payload:O.payload,callback:O.callback,next:null},ge===null?(Q=ge=Re,T=be):ge=ge.next=Re,w|=me;if(O=O.next,O===null){if(O=s.shared.pending,O===null)break;me=O,O=me.next,me.next=null,s.lastBaseUpdate=me,s.shared.pending=null}}while(!0);if(ge===null&&(T=be),s.baseState=T,s.firstBaseUpdate=Q,s.lastBaseUpdate=ge,n=s.shared.interleaved,n!==null){s=n;do w|=s.lane,s=s.next;while(s!==n)}else f===null&&(s.shared.lanes=0);ji|=w,e.lanes=w,e.memoizedState=be}}function Pd(e,n,o){if(e=n.effects,n.effects=null,e!==null)for(n=0;no?o:4,e(!0);var u=yc.transition;yc.transition={};try{e(!1),n()}finally{vt=o,yc.transition=u}}function Kd(){return Ur().memoizedState}function ih(e,n,o){var u=ai(e);if(o={lane:u,action:o,hasEagerState:!1,eagerState:null,next:null},Wd(e))Yd(n,o);else if(o=Ed(e,n,o,u),o!==null){var s=yr();ta(o,e,u,s),Gd(o,n,u)}}function oh(e,n,o){var u=ai(e),s={lane:u,action:o,hasEagerState:!1,eagerState:null,next:null};if(Wd(e))Yd(n,s);else{var f=e.alternate;if(e.lanes===0&&(f===null||f.lanes===0)&&(f=n.lastRenderedReducer,f!==null))try{var w=n.lastRenderedState,O=f(w,o);if(s.hasEagerState=!0,s.eagerState=O,Ir(O,w)){var T=n.interleaved;T===null?(s.next=s,dc(n)):(s.next=T.next,T.next=s),n.interleaved=s;return}}catch{}finally{}o=Ed(e,n,s,u),o!==null&&(s=yr(),ta(o,e,u,s),Gd(o,n,u))}}function Wd(e){var n=e.alternate;return e===en||n!==null&&n===en}function Yd(e,n){Qo=gu=!0;var o=e.pending;o===null?n.next=n:(n.next=o.next,o.next=n),e.pending=n}function Gd(e,n,o){if(o&4194240){var u=n.lanes;u&=e.pendingLanes,o|=u,n.lanes=o,ar(e,o)}}var xu={readContext:Vr,useCallback:lr,useContext:lr,useEffect:lr,useImperativeHandle:lr,useInsertionEffect:lr,useLayoutEffect:lr,useMemo:lr,useReducer:lr,useRef:lr,useState:lr,useDebugValue:lr,useDeferredValue:lr,useTransition:lr,useMutableSource:lr,useSyncExternalStore:lr,useId:lr,unstable_isNewReconciler:!1},lh={readContext:Vr,useCallback:function(e,n){return ma().memoizedState=[e,n===void 0?null:n],e},useContext:Vr,useEffect:Ad,useImperativeHandle:function(e,n,o){return o=o!=null?o.concat([e]):null,yu(4194308,4,Bd.bind(null,n,e),o)},useLayoutEffect:function(e,n){return yu(4194308,4,e,n)},useInsertionEffect:function(e,n){return yu(4,2,e,n)},useMemo:function(e,n){var o=ma();return n=n===void 0?null:n,e=e(),o.memoizedState=[e,n],e},useReducer:function(e,n,o){var u=ma();return n=o!==void 0?o(n):n,u.memoizedState=u.baseState=n,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},u.queue=e,e=e.dispatch=ih.bind(null,en,e),[u.memoizedState,e]},useRef:function(e){var n=ma();return e={current:e},n.memoizedState=e},useState:Ld,useDebugValue:kc,useDeferredValue:function(e){return ma().memoizedState=e},useTransition:function(){var e=Ld(!1),n=e[0];return e=ah.bind(null,e[1]),ma().memoizedState=e,[n,e]},useMutableSource:function(){},useSyncExternalStore:function(e,n,o){var u=en,s=ma();if(Xt){if(o===void 0)throw Error(t(407));o=o()}else{if(o=n(),Fn===null)throw Error(t(349));Mi&30||Id(u,n,o)}s.memoizedState=o;var f={value:o,getSnapshot:n};return s.queue=f,Ad(Md.bind(null,u,f,e),[e]),u.flags|=2048,qo(9,Td.bind(null,u,f,o,n),void 0,null),o},useId:function(){var e=ma(),n=Fn.identifierPrefix;if(Xt){var o=Da,u=Oa;o=(u&~(1<<32-Fe(u)-1)).toString(32)+o,n=":"+n+"R"+o,o=Xo++,0<\/script>",e=e.removeChild(e.firstChild)):typeof u.is=="string"?e=w.createElement(o,{is:u.is}):(e=w.createElement(o),o==="select"&&(w=e,u.multiple?w.multiple=!0:u.size&&(w.size=u.size))):e=w.createElementNS(e,o),e[da]=n,e[Vo]=u,mp(e,n,!1,!1),n.stateNode=e;e:{switch(w=er(o,u),o){case"dialog":bt("cancel",e),bt("close",e),s=u;break;case"iframe":case"object":case"embed":bt("load",e),s=u;break;case"video":case"audio":for(s=0;sfo&&(n.flags|=128,u=!0,Jo(f,!1),n.lanes=4194304)}else{if(!u)if(e=vu(w),e!==null){if(n.flags|=128,u=!0,o=e.updateQueue,o!==null&&(n.updateQueue=o,n.flags|=4),Jo(f,!0),f.tail===null&&f.tailMode==="hidden"&&!w.alternate&&!Xt)return ur(n),null}else 2*Pe()-f.renderingStartTime>fo&&o!==1073741824&&(n.flags|=128,u=!0,Jo(f,!1),n.lanes=4194304);f.isBackwards?(w.sibling=n.child,n.child=w):(o=f.last,o!==null?o.sibling=w:n.child=w,f.last=w)}return f.tail!==null?(n=f.tail,f.rendering=n,f.tail=n.sibling,f.renderingStartTime=Pe(),n.sibling=null,o=Jt.current,$t(Jt,u?o&1|2:o&1),n):(ur(n),null);case 22:case 23:return Qc(),u=n.memoizedState!==null,e!==null&&e.memoizedState!==null!==u&&(n.flags|=8192),u&&n.mode&1?Rr&1073741824&&(ur(n),n.subtreeFlags&6&&(n.flags|=8192)):ur(n),null;case 24:return null;case 25:return null}throw Error(t(156,n.tag))}function vh(e,n){switch(ac(n),n.tag){case 1:return Sr(n.type)&&au(),e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 3:return lo(),Wt(br),Wt(or),gc(),e=n.flags,e&65536&&!(e&128)?(n.flags=e&-65537|128,n):null;case 5:return vc(n),null;case 13:if(Wt(Jt),e=n.memoizedState,e!==null&&e.dehydrated!==null){if(n.alternate===null)throw Error(t(340));ro()}return e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 19:return Wt(Jt),null;case 4:return lo(),null;case 10:return cc(n.type._context),null;case 22:case 23:return Qc(),null;case 24:return null;default:return null}}var Cu=!1,sr=!1,hh=typeof WeakSet=="function"?WeakSet:Set,Ae=null;function so(e,n){var o=e.ref;if(o!==null)if(typeof o=="function")try{o(null)}catch(u){on(e,n,u)}else o.current=null}function Ac(e,n,o){try{o()}catch(u){on(e,n,u)}}var gp=!1;function gh(e,n){if(Qs=hi,e=b(),F(e)){if("selectionStart"in e)var o={start:e.selectionStart,end:e.selectionEnd};else e:{o=(o=e.ownerDocument)&&o.defaultView||window;var u=o.getSelection&&o.getSelection();if(u&&u.rangeCount!==0){o=u.anchorNode;var s=u.anchorOffset,f=u.focusNode;u=u.focusOffset;try{o.nodeType,f.nodeType}catch{o=null;break e}var w=0,O=-1,T=-1,Q=0,ge=0,be=e,me=null;t:for(;;){for(var Re;be!==o||s!==0&&be.nodeType!==3||(O=w+s),be!==f||u!==0&&be.nodeType!==3||(T=w+u),be.nodeType===3&&(w+=be.nodeValue.length),(Re=be.firstChild)!==null;)me=be,be=Re;for(;;){if(be===e)break t;if(me===o&&++Q===s&&(O=w),me===f&&++ge===u&&(T=w),(Re=be.nextSibling)!==null)break;be=me,me=be.parentNode}be=Re}o=O===-1||T===-1?null:{start:O,end:T}}else o=null}o=o||{start:0,end:0}}else o=null;for(Xs={focusedElem:e,selectionRange:o},hi=!1,Ae=n;Ae!==null;)if(n=Ae,e=n.child,(n.subtreeFlags&1028)!==0&&e!==null)e.return=n,Ae=e;else for(;Ae!==null;){n=Ae;try{var $e=n.alternate;if(n.flags&1024)switch(n.tag){case 0:case 11:case 15:break;case 1:if($e!==null){var ze=$e.memoizedProps,fn=$e.memoizedState,H=n.stateNode,j=H.getSnapshotBeforeUpdate(n.elementType===n.type?ze:qr(n.type,ze),fn);H.__reactInternalSnapshotBeforeUpdate=j}break;case 3:var Y=n.stateNode.containerInfo;Y.nodeType===1?Y.textContent="":Y.nodeType===9&&Y.documentElement&&Y.removeChild(Y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(t(163))}}catch(Ce){on(n,n.return,Ce)}if(e=n.sibling,e!==null){e.return=n.return,Ae=e;break}Ae=n.return}return $e=gp,gp=!1,$e}function el(e,n,o){var u=n.updateQueue;if(u=u!==null?u.lastEffect:null,u!==null){var s=u=u.next;do{if((s.tag&e)===e){var f=s.destroy;s.destroy=void 0,f!==void 0&&Ac(n,o,f)}s=s.next}while(s!==u)}}function ku(e,n){if(n=n.updateQueue,n=n!==null?n.lastEffect:null,n!==null){var o=n=n.next;do{if((o.tag&e)===e){var u=o.create;o.destroy=u()}o=o.next}while(o!==n)}}function _c(e){var n=e.ref;if(n!==null){var o=e.stateNode;switch(e.tag){case 5:e=o;break;default:e=o}typeof n=="function"?n(e):n.current=e}}function yp(e){var n=e.alternate;n!==null&&(e.alternate=null,yp(n)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(n=e.stateNode,n!==null&&(delete n[da],delete n[Vo],delete n[ec],delete n[Jv],delete n[eh])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function wp(e){return e.tag===5||e.tag===3||e.tag===4}function xp(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||wp(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function $c(e,n,o){var u=e.tag;if(u===5||u===6)e=e.stateNode,n?o.nodeType===8?o.parentNode.insertBefore(e,n):o.insertBefore(e,n):(o.nodeType===8?(n=o.parentNode,n.insertBefore(e,o)):(n=o,n.appendChild(e)),o=o._reactRootContainer,o!=null||n.onclick!==null||(n.onclick=nu));else if(u!==4&&(e=e.child,e!==null))for($c(e,n,o),e=e.sibling;e!==null;)$c(e,n,o),e=e.sibling}function Bc(e,n,o){var u=e.tag;if(u===5||u===6)e=e.stateNode,n?o.insertBefore(e,n):o.appendChild(e);else if(u!==4&&(e=e.child,e!==null))for(Bc(e,n,o),e=e.sibling;e!==null;)Bc(e,n,o),e=e.sibling}var Yn=null,Jr=!1;function ti(e,n,o){for(o=o.child;o!==null;)bp(e,n,o),o=o.sibling}function bp(e,n,o){if(ee&&typeof ee.onCommitFiberUnmount=="function")try{ee.onCommitFiberUnmount(re,o)}catch{}switch(o.tag){case 5:sr||so(o,n);case 6:var u=Yn,s=Jr;Yn=null,ti(e,n,o),Yn=u,Jr=s,Yn!==null&&(Jr?(e=Yn,o=o.stateNode,e.nodeType===8?e.parentNode.removeChild(o):e.removeChild(o)):Yn.removeChild(o.stateNode));break;case 18:Yn!==null&&(Jr?(e=Yn,o=o.stateNode,e.nodeType===8?Js(e.parentNode,o):e.nodeType===1&&Js(e,o),vi(e)):Js(Yn,o.stateNode));break;case 4:u=Yn,s=Jr,Yn=o.stateNode.containerInfo,Jr=!0,ti(e,n,o),Yn=u,Jr=s;break;case 0:case 11:case 14:case 15:if(!sr&&(u=o.updateQueue,u!==null&&(u=u.lastEffect,u!==null))){s=u=u.next;do{var f=s,w=f.destroy;f=f.tag,w!==void 0&&(f&2||f&4)&&Ac(o,n,w),s=s.next}while(s!==u)}ti(e,n,o);break;case 1:if(!sr&&(so(o,n),u=o.stateNode,typeof u.componentWillUnmount=="function"))try{u.props=o.memoizedProps,u.state=o.memoizedState,u.componentWillUnmount()}catch(O){on(o,n,O)}ti(e,n,o);break;case 21:ti(e,n,o);break;case 22:o.mode&1?(sr=(u=sr)||o.memoizedState!==null,ti(e,n,o),sr=u):ti(e,n,o);break;default:ti(e,n,o)}}function Sp(e){var n=e.updateQueue;if(n!==null){e.updateQueue=null;var o=e.stateNode;o===null&&(o=e.stateNode=new hh),n.forEach(function(u){var s=Ph.bind(null,e,u);o.has(u)||(o.add(u),u.then(s,s))})}}function ea(e,n){var o=n.deletions;if(o!==null)for(var u=0;us&&(s=w),u&=~f}if(u=s,u=Pe()-u,u=(120>u?120:480>u?480:1080>u?1080:1920>u?1920:3e3>u?3e3:4320>u?4320:1960*wh(u/1960))-u,10e?16:e,ri===null)var u=!1;else{if(e=ri,ri=null,Iu=0,Et&6)throw Error(t(331));var s=Et;for(Et|=4,Ae=e.current;Ae!==null;){var f=Ae,w=f.child;if(Ae.flags&16){var O=f.deletions;if(O!==null){for(var T=0;TPe()-Vc?Li(e,0):Hc|=o),kr(e,n)}function Lp(e,n){n===0&&(e.mode&1?(n=Nt,Nt<<=1,!(Nt&130023424)&&(Nt=4194304)):n=1);var o=yr();e=Na(e,n),e!==null&&(qt(e,n,o),kr(e,o))}function kh(e){var n=e.memoizedState,o=0;n!==null&&(o=n.retryLane),Lp(e,o)}function Ph(e,n){var o=0;switch(e.tag){case 13:var u=e.stateNode,s=e.memoizedState;s!==null&&(o=s.retryLane);break;case 19:u=e.stateNode;break;default:throw Error(t(314))}u!==null&&u.delete(n),Lp(e,o)}var Fp;Fp=function(e,n,o){if(e!==null)if(e.memoizedProps!==n.pendingProps||br.current)Er=!0;else{if(!(e.lanes&o)&&!(n.flags&128))return Er=!1,ph(e,n,o);Er=!!(e.flags&131072)}else Er=!1,Xt&&n.flags&1048576&&vd(n,uu,n.index);switch(n.lanes=0,n.tag){case 2:var u=n.type;Eu(e,n),e=n.pendingProps;var s=eo(n,or.current);oo(n,o),s=xc(null,n,u,e,s,o);var f=bc();return n.flags|=1,typeof s=="object"&&s!==null&&typeof s.render=="function"&&s.$$typeof===void 0?(n.tag=1,n.memoizedState=null,n.updateQueue=null,Sr(u)?(f=!0,iu(n)):f=!1,n.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,pc(n),s.updater=bu,n.stateNode=s,s._reactInternals=n,Oc(n,u,e,o),n=Tc(null,n,u,!0,f,o)):(n.tag=0,Xt&&f&&rc(n),gr(null,n,s,o),n=n.child),n;case 16:u=n.elementType;e:{switch(Eu(e,n),e=n.pendingProps,s=u._init,u=s(u._payload),n.type=u,s=n.tag=Dh(u),e=qr(u,e),s){case 0:n=Ic(null,n,u,e,o);break e;case 1:n=up(null,n,u,e,o);break e;case 11:n=rp(null,n,u,e,o);break e;case 14:n=ap(null,n,u,qr(u.type,e),o);break e}throw Error(t(306,u,""))}return n;case 0:return u=n.type,s=n.pendingProps,s=n.elementType===u?s:qr(u,s),Ic(e,n,u,s,o);case 1:return u=n.type,s=n.pendingProps,s=n.elementType===u?s:qr(u,s),up(e,n,u,s,o);case 3:e:{if(sp(n),e===null)throw Error(t(387));u=n.pendingProps,f=n.memoizedState,s=f.element,Cd(e,n),mu(n,u,null,o);var w=n.memoizedState;if(u=w.element,f.isDehydrated)if(f={element:u,isDehydrated:!1,cache:w.cache,pendingSuspenseBoundaries:w.pendingSuspenseBoundaries,transitions:w.transitions},n.updateQueue.baseState=f,n.memoizedState=f,n.flags&256){s=uo(Error(t(423)),n),n=cp(e,n,u,o,s);break e}else if(u!==s){s=uo(Error(t(424)),n),n=cp(e,n,u,o,s);break e}else for(jr=Qa(n.stateNode.containerInfo.firstChild),Mr=n,Xt=!0,Zr=null,o=Sd(n,null,u,o),n.child=o;o;)o.flags=o.flags&-3|4096,o=o.sibling;else{if(ro(),u===s){n=Ta(e,n,o);break e}gr(e,n,u,o)}n=n.child}return n;case 5:return Od(n),e===null&&oc(n),u=n.type,s=n.pendingProps,f=e!==null?e.memoizedProps:null,w=s.children,Zs(u,s)?w=null:f!==null&&Zs(u,f)&&(n.flags|=32),lp(e,n),gr(e,n,w,o),n.child;case 6:return e===null&&oc(n),null;case 13:return fp(e,n,o);case 4:return mc(n,n.stateNode.containerInfo),u=n.pendingProps,e===null?n.child=ao(n,null,u,o):gr(e,n,u,o),n.child;case 11:return u=n.type,s=n.pendingProps,s=n.elementType===u?s:qr(u,s),rp(e,n,u,s,o);case 7:return gr(e,n,n.pendingProps,o),n.child;case 8:return gr(e,n,n.pendingProps.children,o),n.child;case 12:return gr(e,n,n.pendingProps.children,o),n.child;case 10:e:{if(u=n.type._context,s=n.pendingProps,f=n.memoizedProps,w=s.value,$t(fu,u._currentValue),u._currentValue=w,f!==null)if(Ir(f.value,w)){if(f.children===s.children&&!br.current){n=Ta(e,n,o);break e}}else for(f=n.child,f!==null&&(f.return=n);f!==null;){var O=f.dependencies;if(O!==null){w=f.child;for(var T=O.firstContext;T!==null;){if(T.context===u){if(f.tag===1){T=Ia(-1,o&-o),T.tag=2;var Q=f.updateQueue;if(Q!==null){Q=Q.shared;var ge=Q.pending;ge===null?T.next=T:(T.next=ge.next,ge.next=T),Q.pending=T}}f.lanes|=o,T=f.alternate,T!==null&&(T.lanes|=o),fc(f.return,o,n),O.lanes|=o;break}T=T.next}}else if(f.tag===10)w=f.type===n.type?null:f.child;else if(f.tag===18){if(w=f.return,w===null)throw Error(t(341));w.lanes|=o,O=w.alternate,O!==null&&(O.lanes|=o),fc(w,o,n),w=f.sibling}else w=f.child;if(w!==null)w.return=f;else for(w=f;w!==null;){if(w===n){w=null;break}if(f=w.sibling,f!==null){f.return=w.return,w=f;break}w=w.return}f=w}gr(e,n,s.children,o),n=n.child}return n;case 9:return s=n.type,u=n.pendingProps.children,oo(n,o),s=Vr(s),u=u(s),n.flags|=1,gr(e,n,u,o),n.child;case 14:return u=n.type,s=qr(u,n.pendingProps),s=qr(u.type,s),ap(e,n,u,s,o);case 15:return ip(e,n,n.type,n.pendingProps,o);case 17:return u=n.type,s=n.pendingProps,s=n.elementType===u?s:qr(u,s),Eu(e,n),n.tag=1,Sr(u)?(e=!0,iu(n)):e=!1,oo(n,o),Xd(n,u,s),Oc(n,u,s,o),Tc(null,n,u,!0,e,o);case 19:return pp(e,n,o);case 22:return op(e,n,o)}throw Error(t(156,n.tag))};function Ap(e,n){return ke(e,n)}function Oh(e,n,o,u){this.tag=e,this.key=o,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=u,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Wr(e,n,o,u){return new Oh(e,n,o,u)}function Zc(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Dh(e){if(typeof e=="function")return Zc(e)?1:0;if(e!=null){if(e=e.$$typeof,e===_e)return 11;if(e===se)return 14}return 2}function oi(e,n){var o=e.alternate;return o===null?(o=Wr(e.tag,n,e.key,e.mode),o.elementType=e.elementType,o.type=e.type,o.stateNode=e.stateNode,o.alternate=e,e.alternate=o):(o.pendingProps=n,o.type=e.type,o.flags=0,o.subtreeFlags=0,o.deletions=null),o.flags=e.flags&14680064,o.childLanes=e.childLanes,o.lanes=e.lanes,o.child=e.child,o.memoizedProps=e.memoizedProps,o.memoizedState=e.memoizedState,o.updateQueue=e.updateQueue,n=e.dependencies,o.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},o.sibling=e.sibling,o.index=e.index,o.ref=e.ref,o}function Ru(e,n,o,u,s,f){var w=2;if(u=e,typeof e=="function")Zc(e)&&(w=1);else if(typeof e=="string")w=5;else e:switch(e){case ye:return Ai(o.children,s,f,n);case De:w=8,s|=8;break;case z:return e=Wr(12,o,n,s|2),e.elementType=z,e.lanes=f,e;case q:return e=Wr(13,o,n,s),e.elementType=q,e.lanes=f,e;case Ie:return e=Wr(19,o,n,s),e.elementType=Ie,e.lanes=f,e;case Ve:return Lu(o,s,f,n);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case U:w=10;break e;case oe:w=9;break e;case _e:w=11;break e;case se:w=14;break e;case Ye:w=16,u=null;break e}throw Error(t(130,e==null?e:typeof e,""))}return n=Wr(w,o,n,s),n.elementType=e,n.type=u,n.lanes=f,n}function Ai(e,n,o,u){return e=Wr(7,e,u,n),e.lanes=o,e}function Lu(e,n,o,u){return e=Wr(22,e,u,n),e.elementType=Ve,e.lanes=o,e.stateNode={isHidden:!1},e}function qc(e,n,o){return e=Wr(6,e,null,n),e.lanes=o,e}function Jc(e,n,o){return n=Wr(4,e.children!==null?e.children:[],e.key,n),n.lanes=o,n.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},n}function Nh(e,n,o,u,s){this.tag=n,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=sn(0),this.expirationTimes=sn(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=sn(0),this.identifierPrefix=u,this.onRecoverableError=s,this.mutableSourceEagerHydrationData=null}function ef(e,n,o,u,s,f,w,O,T){return e=new Nh(e,n,o,O,T),n===1?(n=1,f===!0&&(n|=8)):n=0,f=Wr(3,null,null,n),e.current=f,f.stateNode=e,f.memoizedState={element:u,isDehydrated:o,cache:null,transitions:null,pendingSuspenseBoundaries:null},pc(f),e}function Ih(e,n,o){var u=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(i){console.error(i)}}return a(),lf.exports=Bh(),lf.exports}var Zp;function zh(){if(Zp)return Hu;Zp=1;var a=Km();return Hu.createRoot=a.createRoot,Hu.hydrateRoot=a.hydrateRoot,Hu}var Hh=zh(),m=Wf();const wr=Um(m);var ol={},qp;function Vh(){if(qp)return ol;qp=1,Object.defineProperty(ol,"__esModule",{value:!0}),ol.parse=h,ol.serialize=y;const a=/^[\u0021-\u003A\u003C\u003E-\u007E]+$/,i=/^[\u0021-\u003A\u003C-\u007E]*$/,t=/^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i,l=/^[\u0020-\u003A\u003D-\u007E]*$/,r=Object.prototype.toString,d=(()=>{const N=function(){};return N.prototype=Object.create(null),N})();function h(N,I){const M=new d,D=N.length;if(D<2)return M;const B=(I==null?void 0:I.decode)||k;let _=0;do{const $=N.indexOf("=",_);if($===-1)break;const W=N.indexOf(";",_),he=W===-1?D:W;if($>he){_=N.lastIndexOf(";",$-1)+1;continue}const Se=v(N,_,$),Oe=g(N,$,Se),ye=N.slice(Se,Oe);if(M[ye]===void 0){let De=v(N,$+1,he),z=g(N,he,De);const U=B(N.slice(De,z));M[ye]=U}_=he+1}while(_M;){const D=N.charCodeAt(--I);if(D!==32&&D!==9)return I+1}return M}function y(N,I,M){const D=(M==null?void 0:M.encode)||encodeURIComponent;if(!a.test(N))throw new TypeError(`argument name is invalid: ${N}`);const B=D(I);if(!i.test(B))throw new TypeError(`argument val is invalid: ${I}`);let _=N+"="+B;if(!M)return _;if(M.maxAge!==void 0){if(!Number.isInteger(M.maxAge))throw new TypeError(`option maxAge is invalid: ${M.maxAge}`);_+="; Max-Age="+M.maxAge}if(M.domain){if(!t.test(M.domain))throw new TypeError(`option domain is invalid: ${M.domain}`);_+="; Domain="+M.domain}if(M.path){if(!l.test(M.path))throw new TypeError(`option path is invalid: ${M.path}`);_+="; Path="+M.path}if(M.expires){if(!E(M.expires)||!Number.isFinite(M.expires.valueOf()))throw new TypeError(`option expires is invalid: ${M.expires}`);_+="; Expires="+M.expires.toUTCString()}if(M.httpOnly&&(_+="; HttpOnly"),M.secure&&(_+="; Secure"),M.partitioned&&(_+="; Partitioned"),M.priority)switch(typeof M.priority=="string"?M.priority.toLowerCase():void 0){case"low":_+="; Priority=Low";break;case"medium":_+="; Priority=Medium";break;case"high":_+="; Priority=High";break;default:throw new TypeError(`option priority is invalid: ${M.priority}`)}if(M.sameSite)switch(typeof M.sameSite=="string"?M.sameSite.toLowerCase():M.sameSite){case!0:case"strict":_+="; SameSite=Strict";break;case"lax":_+="; SameSite=Lax";break;case"none":_+="; SameSite=None";break;default:throw new TypeError(`option sameSite is invalid: ${M.sameSite}`)}return _}function k(N){if(N.indexOf("%")===-1)return N;try{return decodeURIComponent(N)}catch{return N}}function E(N){return r.call(N)==="[object Date]"}return ol}Vh();/** - * react-router v7.1.1 - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */var Jp="popstate";function Uh(a={}){function i(l,r){let{pathname:d,search:h,hash:v}=l.location;return hf("",{pathname:d,search:h,hash:v},r.state&&r.state.usr||null,r.state&&r.state.key||"default")}function t(l,r){return typeof r=="string"?r:yl(r)}return Wh(i,t,null,a)}function nn(a,i){if(a===!1||a===null||typeof a>"u")throw new Error(i)}function ya(a,i){if(!a){typeof console<"u"&&console.warn(i);try{throw new Error(i)}catch{}}}function Kh(){return Math.random().toString(36).substring(2,10)}function em(a,i){return{usr:a.state,key:a.key,idx:i}}function hf(a,i,t=null,l){return{pathname:typeof a=="string"?a:a.pathname,search:"",hash:"",...typeof i=="string"?Po(i):i,state:t,key:i&&i.key||l||Kh()}}function yl({pathname:a="/",search:i="",hash:t=""}){return i&&i!=="?"&&(a+=i.charAt(0)==="?"?i:"?"+i),t&&t!=="#"&&(a+=t.charAt(0)==="#"?t:"#"+t),a}function Po(a){let i={};if(a){let t=a.indexOf("#");t>=0&&(i.hash=a.substring(t),a=a.substring(0,t));let l=a.indexOf("?");l>=0&&(i.search=a.substring(l),a=a.substring(0,l)),a&&(i.pathname=a)}return i}function Wh(a,i,t,l={}){let{window:r=document.defaultView,v5Compat:d=!1}=l,h=r.history,v="POP",g=null,y=k();y==null&&(y=0,h.replaceState({...h.state,idx:y},""));function k(){return(h.state||{idx:null}).idx}function E(){v="POP";let B=k(),_=B==null?null:B-y;y=B,g&&g({action:v,location:D.location,delta:_})}function N(B,_){v="PUSH";let $=hf(D.location,B,_);y=k()+1;let W=em($,y),he=D.createHref($);try{h.pushState(W,"",he)}catch(Se){if(Se instanceof DOMException&&Se.name==="DataCloneError")throw Se;r.location.assign(he)}d&&g&&g({action:v,location:D.location,delta:1})}function I(B,_){v="REPLACE";let $=hf(D.location,B,_);y=k();let W=em($,y),he=D.createHref($);h.replaceState(W,"",he),d&&g&&g({action:v,location:D.location,delta:0})}function M(B){let _=r.location.origin!=="null"?r.location.origin:r.location.href,$=typeof B=="string"?B:yl(B);return $=$.replace(/ $/,"%20"),nn(_,`No window.location.(origin|href) available to create URL for href: ${$}`),new URL($,_)}let D={get action(){return v},get location(){return a(r,h)},listen(B){if(g)throw new Error("A history only accepts one active listener");return r.addEventListener(Jp,E),g=B,()=>{r.removeEventListener(Jp,E),g=null}},createHref(B){return i(r,B)},createURL:M,encodeLocation(B){let _=M(B);return{pathname:_.pathname,search:_.search,hash:_.hash}},push:N,replace:I,go(B){return h.go(B)}};return D}function Wm(a,i,t="/"){return Yh(a,i,t,!1)}function Yh(a,i,t,l){let r=typeof i=="string"?Po(i):i,d=ci(r.pathname||"/",t);if(d==null)return null;let h=Ym(a);Gh(h);let v=null;for(let g=0;v==null&&g{let g={relativePath:v===void 0?d.path||"":v,caseSensitive:d.caseSensitive===!0,childrenIndex:h,route:d};g.relativePath.startsWith("/")&&(nn(g.relativePath.startsWith(l),`Absolute route path "${g.relativePath}" nested under path "${l}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),g.relativePath=g.relativePath.slice(l.length));let y=Aa([l,g.relativePath]),k=t.concat(g);d.children&&d.children.length>0&&(nn(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${y}".`),Ym(d.children,i,k,y)),!(d.path==null&&!d.index)&&i.push({path:y,score:tg(y,d.index),routesMeta:k})};return a.forEach((d,h)=>{var v;if(d.path===""||!((v=d.path)!=null&&v.includes("?")))r(d,h);else for(let g of Gm(d.path))r(d,h,g)}),i}function Gm(a){let i=a.split("/");if(i.length===0)return[];let[t,...l]=i,r=t.endsWith("?"),d=t.replace(/\?$/,"");if(l.length===0)return r?[d,""]:[d];let h=Gm(l.join("/")),v=[];return v.push(...h.map(g=>g===""?d:[d,g].join("/"))),r&&v.push(...h),v.map(g=>a.startsWith("/")&&g===""?"/":g)}function Gh(a){a.sort((i,t)=>i.score!==t.score?t.score-i.score:ng(i.routesMeta.map(l=>l.childrenIndex),t.routesMeta.map(l=>l.childrenIndex)))}var Qh=/^:[\w-]+$/,Xh=3,Zh=2,qh=1,Jh=10,eg=-2,tm=a=>a==="*";function tg(a,i){let t=a.split("/"),l=t.length;return t.some(tm)&&(l+=eg),i&&(l+=Zh),t.filter(r=>!tm(r)).reduce((r,d)=>r+(Qh.test(d)?Xh:d===""?qh:Jh),l)}function ng(a,i){return a.length===i.length&&a.slice(0,-1).every((l,r)=>l===i[r])?a[a.length-1]-i[i.length-1]:0}function rg(a,i,t=!1){let{routesMeta:l}=a,r={},d="/",h=[];for(let v=0;v{if(k==="*"){let M=v[N]||"";h=d.slice(0,d.length-M.length).replace(/(.)\/+$/,"$1")}const I=v[N];return E&&!I?y[k]=void 0:y[k]=(I||"").replace(/%2F/g,"/"),y},{}),pathname:d,pathnameBase:h,pattern:a}}function ag(a,i=!1,t=!0){ya(a==="*"||!a.endsWith("*")||a.endsWith("/*"),`Route path "${a}" will be treated as if it were "${a.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${a.replace(/\*$/,"/*")}".`);let l=[],r="^"+a.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(h,v,g)=>(l.push({paramName:v,isOptional:g!=null}),g?"/?([^\\/]+)?":"/([^\\/]+)"));return a.endsWith("*")?(l.push({paramName:"*"}),r+=a==="*"||a==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):t?r+="\\/*$":a!==""&&a!=="/"&&(r+="(?:(?=\\/|$))"),[new RegExp(r,i?void 0:"i"),l]}function ig(a){try{return a.split("/").map(i=>decodeURIComponent(i).replace(/\//g,"%2F")).join("/")}catch(i){return ya(!1,`The URL path "${a}" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent encoding (${i}).`),a}}function ci(a,i){if(i==="/")return a;if(!a.toLowerCase().startsWith(i.toLowerCase()))return null;let t=i.endsWith("/")?i.length-1:i.length,l=a.charAt(t);return l&&l!=="/"?null:a.slice(t)||"/"}function og(a,i="/"){let{pathname:t,search:l="",hash:r=""}=typeof a=="string"?Po(a):a;return{pathname:t?t.startsWith("/")?t:lg(t,i):i,search:cg(l),hash:fg(r)}}function lg(a,i){let t=i.replace(/\/+$/,"").split("/");return a.split("/").forEach(r=>{r===".."?t.length>1&&t.pop():r!=="."&&t.push(r)}),t.length>1?t.join("/"):"/"}function cf(a,i,t,l){return`Cannot include a '${a}' character in a manually specified \`to.${i}\` field [${JSON.stringify(l)}]. Please separate it out to the \`to.${t}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ug(a){return a.filter((i,t)=>t===0||i.route.path&&i.route.path.length>0)}function Qm(a){let i=ug(a);return i.map((t,l)=>l===i.length-1?t.pathname:t.pathnameBase)}function Xm(a,i,t,l=!1){let r;typeof a=="string"?r=Po(a):(r={...a},nn(!r.pathname||!r.pathname.includes("?"),cf("?","pathname","search",r)),nn(!r.pathname||!r.pathname.includes("#"),cf("#","pathname","hash",r)),nn(!r.search||!r.search.includes("#"),cf("#","search","hash",r)));let d=a===""||r.pathname==="",h=d?"/":r.pathname,v;if(h==null)v=t;else{let E=i.length-1;if(!l&&h.startsWith("..")){let N=h.split("/");for(;N[0]==="..";)N.shift(),E-=1;r.pathname=N.join("/")}v=E>=0?i[E]:"/"}let g=og(r,v),y=h&&h!=="/"&&h.endsWith("/"),k=(d||h===".")&&t.endsWith("/");return!g.pathname.endsWith("/")&&(y||k)&&(g.pathname+="/"),g}var Aa=a=>a.join("/").replace(/\/\/+/g,"/"),sg=a=>a.replace(/\/+$/,"").replace(/^\/*/,"/"),cg=a=>!a||a==="?"?"":a.startsWith("?")?a:"?"+a,fg=a=>!a||a==="#"?"":a.startsWith("#")?a:"#"+a;function dg(a){return a!=null&&typeof a.status=="number"&&typeof a.statusText=="string"&&typeof a.internal=="boolean"&&"data"in a}var Zm=["POST","PUT","PATCH","DELETE"];new Set(Zm);var pg=["GET",...Zm];new Set(pg);var Oo=m.createContext(null);Oo.displayName="DataRouter";var vs=m.createContext(null);vs.displayName="DataRouterState";var qm=m.createContext({isTransitioning:!1});qm.displayName="ViewTransition";var mg=m.createContext(new Map);mg.displayName="Fetchers";var vg=m.createContext(null);vg.displayName="Await";var wa=m.createContext(null);wa.displayName="Navigation";var Il=m.createContext(null);Il.displayName="Location";var _a=m.createContext({outlet:null,matches:[],isDataRoute:!1});_a.displayName="Route";var Yf=m.createContext(null);Yf.displayName="RouteError";function hg(a,{relative:i}={}){nn(Tl(),"useHref() may be used only in the context of a component.");let{basename:t,navigator:l}=m.useContext(wa),{hash:r,pathname:d,search:h}=Ml(a,{relative:i}),v=d;return t!=="/"&&(v=d==="/"?t:Aa([t,d])),l.createHref({pathname:v,search:h,hash:r})}function Tl(){return m.useContext(Il)!=null}function Hi(){return nn(Tl(),"useLocation() may be used only in the context of a component."),m.useContext(Il).location}var Jm="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function ev(a){m.useContext(wa).static||m.useLayoutEffect(a)}function gg(){let{isDataRoute:a}=m.useContext(_a);return a?Ig():yg()}function yg(){nn(Tl(),"useNavigate() may be used only in the context of a component.");let a=m.useContext(Oo),{basename:i,navigator:t}=m.useContext(wa),{matches:l}=m.useContext(_a),{pathname:r}=Hi(),d=JSON.stringify(Qm(l)),h=m.useRef(!1);return ev(()=>{h.current=!0}),m.useCallback((g,y={})=>{if(ya(h.current,Jm),!h.current)return;if(typeof g=="number"){t.go(g);return}let k=Xm(g,JSON.parse(d),r,y.relative==="path");a==null&&i!=="/"&&(k.pathname=k.pathname==="/"?i:Aa([i,k.pathname])),(y.replace?t.replace:t.push)(k,y.state,y)},[i,t,d,r,a])}m.createContext(null);function Ml(a,{relative:i}={}){let{matches:t}=m.useContext(_a),{pathname:l}=Hi(),r=JSON.stringify(Qm(t));return m.useMemo(()=>Xm(a,JSON.parse(r),l,i==="path"),[a,r,l,i])}function wg(a,i){return tv(a,i)}function tv(a,i,t,l){var _;nn(Tl(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=m.useContext(wa),{matches:d}=m.useContext(_a),h=d[d.length-1],v=h?h.params:{},g=h?h.pathname:"/",y=h?h.pathnameBase:"/",k=h&&h.route;{let $=k&&k.path||"";nv(g,!k||$.endsWith("*")||$.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${g}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let E=Hi(),N;if(i){let $=typeof i=="string"?Po(i):i;nn(y==="/"||((_=$.pathname)==null?void 0:_.startsWith(y)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${y}" but pathname "${$.pathname}" was given in the \`location\` prop.`),N=$}else N=E;let I=N.pathname||"/",M=I;if(y!=="/"){let $=y.replace(/^\//,"").split("/");M="/"+I.replace(/^\//,"").split("/").slice($.length).join("/")}let D=Wm(a,{pathname:M});ya(k||D!=null,`No routes matched location "${N.pathname}${N.search}${N.hash}" `),ya(D==null||D[D.length-1].route.element!==void 0||D[D.length-1].route.Component!==void 0||D[D.length-1].route.lazy!==void 0,`Matched leaf route at location "${N.pathname}${N.search}${N.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let B=Cg(D&&D.map($=>Object.assign({},$,{params:Object.assign({},v,$.params),pathname:Aa([y,r.encodeLocation?r.encodeLocation($.pathname).pathname:$.pathname]),pathnameBase:$.pathnameBase==="/"?y:Aa([y,r.encodeLocation?r.encodeLocation($.pathnameBase).pathname:$.pathnameBase])})),d,t,l);return i&&B?m.createElement(Il.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...N},navigationType:"POP"}},B):B}function xg(){let a=Ng(),i=dg(a)?`${a.status} ${a.statusText}`:a instanceof Error?a.message:JSON.stringify(a),t=a instanceof Error?a.stack:null,l="rgba(200,200,200, 0.5)",r={padding:"0.5rem",backgroundColor:l},d={padding:"2px 4px",backgroundColor:l},h=null;return console.error("Error handled by React Router default ErrorBoundary:",a),h=m.createElement(m.Fragment,null,m.createElement("p",null,"💿 Hey developer 👋"),m.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",m.createElement("code",{style:d},"ErrorBoundary")," or"," ",m.createElement("code",{style:d},"errorElement")," prop on your route.")),m.createElement(m.Fragment,null,m.createElement("h2",null,"Unexpected Application Error!"),m.createElement("h3",{style:{fontStyle:"italic"}},i),t?m.createElement("pre",{style:r},t):null,h)}var bg=m.createElement(xg,null),Sg=class extends m.Component{constructor(a){super(a),this.state={location:a.location,revalidation:a.revalidation,error:a.error}}static getDerivedStateFromError(a){return{error:a}}static getDerivedStateFromProps(a,i){return i.location!==a.location||i.revalidation!=="idle"&&a.revalidation==="idle"?{error:a.error,location:a.location,revalidation:a.revalidation}:{error:a.error!==void 0?a.error:i.error,location:i.location,revalidation:a.revalidation||i.revalidation}}componentDidCatch(a,i){console.error("React Router caught the following error during render",a,i)}render(){return this.state.error!==void 0?m.createElement(_a.Provider,{value:this.props.routeContext},m.createElement(Yf.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function Eg({routeContext:a,match:i,children:t}){let l=m.useContext(Oo);return l&&l.static&&l.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=i.route.id),m.createElement(_a.Provider,{value:a},t)}function Cg(a,i=[],t=null,l=null){if(a==null){if(!t)return null;if(t.errors)a=t.matches;else if(i.length===0&&!t.initialized&&t.matches.length>0)a=t.matches;else return null}let r=a,d=t==null?void 0:t.errors;if(d!=null){let g=r.findIndex(y=>y.route.id&&(d==null?void 0:d[y.route.id])!==void 0);nn(g>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),r=r.slice(0,Math.min(r.length,g+1))}let h=!1,v=-1;if(t)for(let g=0;g=0?r=r.slice(0,v+1):r=[r[0]];break}}}return r.reduceRight((g,y,k)=>{let E,N=!1,I=null,M=null;t&&(E=d&&y.route.id?d[y.route.id]:void 0,I=y.route.errorElement||bg,h&&(v<0&&k===0?(nv("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),N=!0,M=null):v===k&&(N=!0,M=y.route.hydrateFallbackElement||null)));let D=i.concat(r.slice(0,k+1)),B=()=>{let _;return E?_=I:N?_=M:y.route.Component?_=m.createElement(y.route.Component,null):y.route.element?_=y.route.element:_=g,m.createElement(Eg,{match:y,routeContext:{outlet:g,matches:D,isDataRoute:t!=null},children:_})};return t&&(y.route.ErrorBoundary||y.route.errorElement||k===0)?m.createElement(Sg,{location:t.location,revalidation:t.revalidation,component:I,error:E,children:B(),routeContext:{outlet:null,matches:D,isDataRoute:!0}}):B()},null)}function Gf(a){return`${a} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function kg(a){let i=m.useContext(Oo);return nn(i,Gf(a)),i}function Pg(a){let i=m.useContext(vs);return nn(i,Gf(a)),i}function Og(a){let i=m.useContext(_a);return nn(i,Gf(a)),i}function Qf(a){let i=Og(a),t=i.matches[i.matches.length-1];return nn(t.route.id,`${a} can only be used on routes that contain a unique "id"`),t.route.id}function Dg(){return Qf("useRouteId")}function Ng(){var l;let a=m.useContext(Yf),i=Pg("useRouteError"),t=Qf("useRouteError");return a!==void 0?a:(l=i.errors)==null?void 0:l[t]}function Ig(){let{router:a}=kg("useNavigate"),i=Qf("useNavigate"),t=m.useRef(!1);return ev(()=>{t.current=!0}),m.useCallback(async(r,d={})=>{ya(t.current,Jm),t.current&&(typeof r=="number"?a.navigate(r):await a.navigate(r,{fromRouteId:i,...d}))},[a,i])}var nm={};function nv(a,i,t){!i&&!nm[a]&&(nm[a]=!0,ya(!1,t))}m.memo(Tg);function Tg({routes:a,future:i,state:t}){return tv(a,void 0,t,i)}function sl(a){nn(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Mg({basename:a="/",children:i=null,location:t,navigationType:l="POP",navigator:r,static:d=!1}){nn(!Tl(),"You cannot render a inside another . You should never have more than one in your app.");let h=a.replace(/^\/*/,"/"),v=m.useMemo(()=>({basename:h,navigator:r,static:d,future:{}}),[h,r,d]);typeof t=="string"&&(t=Po(t));let{pathname:g="/",search:y="",hash:k="",state:E=null,key:N="default"}=t,I=m.useMemo(()=>{let M=ci(g,h);return M==null?null:{location:{pathname:M,search:y,hash:k,state:E,key:N},navigationType:l}},[h,g,y,k,E,N,l]);return ya(I!=null,` is not able to match the URL "${g}${y}${k}" because it does not start with the basename, so the won't render anything.`),I==null?null:m.createElement(wa.Provider,{value:v},m.createElement(Il.Provider,{children:i,value:I}))}function jg({children:a,location:i}){return wg(gf(a),i)}function gf(a,i=[]){let t=[];return m.Children.forEach(a,(l,r)=>{if(!m.isValidElement(l))return;let d=[...i,r];if(l.type===m.Fragment){t.push.apply(t,gf(l.props.children,d));return}nn(l.type===sl,`[${typeof l.type=="string"?l.type:l.type.name}] is not a component. All component children of must be a or `),nn(!l.props.index||!l.props.children,"An index route cannot have child routes.");let h={id:l.props.id||d.join("-"),caseSensitive:l.props.caseSensitive,element:l.props.element,Component:l.props.Component,index:l.props.index,path:l.props.path,loader:l.props.loader,action:l.props.action,hydrateFallbackElement:l.props.hydrateFallbackElement,HydrateFallback:l.props.HydrateFallback,errorElement:l.props.errorElement,ErrorBoundary:l.props.ErrorBoundary,hasErrorBoundary:l.props.hasErrorBoundary===!0||l.props.ErrorBoundary!=null||l.props.errorElement!=null,shouldRevalidate:l.props.shouldRevalidate,handle:l.props.handle,lazy:l.props.lazy};l.props.children&&(h.children=gf(l.props.children,d)),t.push(h)}),t}var ns="get",rs="application/x-www-form-urlencoded";function hs(a){return a!=null&&typeof a.tagName=="string"}function Rg(a){return hs(a)&&a.tagName.toLowerCase()==="button"}function Lg(a){return hs(a)&&a.tagName.toLowerCase()==="form"}function Fg(a){return hs(a)&&a.tagName.toLowerCase()==="input"}function Ag(a){return!!(a.metaKey||a.altKey||a.ctrlKey||a.shiftKey)}function _g(a,i){return a.button===0&&(!i||i==="_self")&&!Ag(a)}var Vu=null;function $g(){if(Vu===null)try{new FormData(document.createElement("form"),0),Vu=!1}catch{Vu=!0}return Vu}var Bg=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function ff(a){return a!=null&&!Bg.has(a)?(ya(!1,`"${a}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${rs}"`),null):a}function zg(a,i){let t,l,r,d,h;if(Lg(a)){let v=a.getAttribute("action");l=v?ci(v,i):null,t=a.getAttribute("method")||ns,r=ff(a.getAttribute("enctype"))||rs,d=new FormData(a)}else if(Rg(a)||Fg(a)&&(a.type==="submit"||a.type==="image")){let v=a.form;if(v==null)throw new Error('Cannot submit a