Created
May 16, 2025 18:28
-
-
Save MarwanShehata/18fb82b8280edf3d2489878d004bf930 to your computer and use it in GitHub Desktop.
Joblum Board Auto-Apply
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Joblum Board Auto-Apply | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0 | |
// @description Automates applying to frontend web developer jobs on a job board. | |
// @author You | |
// @match *://ru.joblum.com/* | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @grant GM_addStyle | |
// ==/UserScript== | |
;(function () { | |
'use strict' | |
// Default settings | |
const defaultSettings = { | |
positiveKeywords: | |
'frontend, front-end, react, nextjs, next.js, javascript, typescript, vue, angular', | |
negativeKeywords: | |
'native, .NET, Python, Django, PHP, Laravel, mobile, iOS, Android', | |
autoStart: false | |
} | |
// Job stats | |
let jobStats = { | |
jobsScanned: 0, | |
titleMatches: 0, | |
descriptionMatches: 0, | |
applicationsSubmitted: 0, | |
pagesProcessed: 0 | |
} | |
// State | |
let processedJobUrls = new Set() | |
let currentSearchPageUrl = null | |
let isProcessing = false | |
let stopRequested = false | |
let pageChangeTimer = null | |
// Load settings | |
function loadSettings() { | |
const settings = { | |
positiveKeywords: GM_getValue( | |
'positiveKeywords', | |
defaultSettings.positiveKeywords | |
), | |
negativeKeywords: GM_getValue( | |
'negativeKeywords', | |
defaultSettings.negativeKeywords | |
), | |
autoStart: GM_getValue('autoStart', defaultSettings.autoStart) | |
} | |
return { | |
positiveKeywords: settings.positiveKeywords | |
.split(',') | |
.map((k) => k.trim().toLowerCase()) | |
.filter((k) => k), | |
negativeKeywords: settings.negativeKeywords | |
.split(',') | |
.map((k) => k.trim().toLowerCase()) | |
.filter((k) => k), | |
autoStart: settings.autoStart | |
} | |
} | |
// Load saved processed URLs | |
function loadProcessedUrls() { | |
const savedUrls = GM_getValue('processedJobUrls', '[]') | |
try { | |
processedJobUrls = new Set(JSON.parse(savedUrls)) | |
} catch (e) { | |
console.error('Error loading processed URLs:', e) | |
processedJobUrls = new Set() | |
} | |
} | |
// Save processed URLs | |
function saveProcessedUrls() { | |
GM_setValue('processedJobUrls', JSON.stringify([...processedJobUrls])) | |
} | |
let POSITIVE_KEYWORDS = loadSettings().positiveKeywords | |
let NEGATIVE_KEYWORDS = loadSettings().negativeKeywords | |
// Save settings | |
function saveSettings(settings) { | |
GM_setValue('positiveKeywords', settings.positiveKeywords) | |
GM_setValue('negativeKeywords', settings.negativeKeywords) | |
GM_setValue('autoStart', settings.autoStart) | |
POSITIVE_KEYWORDS = settings.positiveKeywords | |
.split(',') | |
.map((k) => k.trim().toLowerCase()) | |
.filter((k) => k) | |
NEGATIVE_KEYWORDS = settings.negativeKeywords | |
.split(',') | |
.map((k) => k.trim().toLowerCase()) | |
.filter((k) => k) | |
} | |
// Save stats | |
function saveStats() { | |
GM_setValue('jobStats', JSON.stringify(jobStats)) | |
} | |
// Load stats | |
function loadStats() { | |
const savedStats = GM_getValue('jobStats', null) | |
if (savedStats) { | |
jobStats = JSON.parse(savedStats) | |
} | |
} | |
// Utility to check if text matches criteria | |
function matchesCriteria(text) { | |
if (!text) return false | |
const lowerText = text.toLowerCase() | |
const hasPositive = POSITIVE_KEYWORDS.some((keyword) => | |
lowerText.includes(keyword) | |
) | |
const hasNegative = NEGATIVE_KEYWORDS.some((keyword) => | |
lowerText.includes(keyword) | |
) | |
return hasPositive && !hasNegative | |
} | |
// Utility to wait for an element | |
async function waitForElement(selector, timeout = 10000) { | |
const start = Date.now() | |
while (Date.now() - start < timeout) { | |
const element = document.querySelector(selector) | |
if (element) return element | |
await new Promise((resolve) => setTimeout(resolve, 100)) | |
} | |
console.log(`Element not found after timeout: ${selector}`) | |
return null | |
} | |
// Utility to wait for a condition | |
async function waitForCondition(condition, timeout = 10000) { | |
const start = Date.now() | |
while (Date.now() - start < timeout) { | |
if (condition()) return true | |
if (stopRequested) return false | |
await new Promise((resolve) => setTimeout(resolve, 100)) | |
} | |
console.log(`Condition not met after timeout`) | |
return false | |
} | |
// Prevent links from opening in new tabs | |
function preventNewTabs() { | |
document.querySelectorAll('a[target="_blank"]').forEach((anchor) => { | |
anchor.removeAttribute('target') | |
}) | |
} | |
// Check if URL is a search results page | |
function isSearchResultsPage() { | |
return !!document.querySelector('.content-card.card-has-jobs') | |
} | |
// Check if URL is a job details page | |
function isJobDetailsPage() { | |
return !!( | |
document.querySelector('h1.job-title') && | |
document.querySelector('span[itemprop="description"]') | |
) | |
} | |
// Check if URL is an application form page | |
function isApplicationFormPage() { | |
return !!document.querySelector('form#w1') | |
} | |
// Determine current page type and start appropriate process | |
async function determinePage() { | |
if (stopRequested) return | |
if (isSearchResultsPage()) { | |
console.log('Detected search results page') | |
await processSearchResults() | |
} else if (isJobDetailsPage()) { | |
console.log('Detected job details page') | |
await processJobDetails(currentSearchPageUrl) | |
} else if (isApplicationFormPage()) { | |
console.log('Detected application form page') | |
await processApplicationForm(currentSearchPageUrl) | |
} else { | |
console.log('Not on a recognized job board page') | |
showNotification('Not on a job board page.', 'warning') | |
isProcessing = false | |
updateUI() | |
} | |
} | |
// Utility to delay execution | |
async function delay(ms) { | |
return new Promise((resolve) => setTimeout(resolve, ms)) | |
} | |
// Process search results page | |
async function processSearchResults() { | |
if (stopRequested) return | |
preventNewTabs() | |
currentSearchPageUrl = window.location.href | |
console.log('Processing search page:', currentSearchPageUrl) | |
GM_setValue('lastSearchPage', currentSearchPageUrl) | |
// Wait 10 seconds for translation | |
console.log('Waiting 10 seconds for page translation...') | |
await delay(10000) // 10-second delay | |
const jobWrappers = document.querySelectorAll('.result-wrp.row') | |
console.log(`Found ${jobWrappers.length} job wrappers`) | |
const jobs = [] | |
jobStats.pagesProcessed++ | |
saveStats() | |
updateUI() | |
for (const wrapper of jobWrappers) { | |
if (stopRequested) return | |
jobStats.jobsScanned++ | |
const titleElement = wrapper.querySelector('.job-title a') | |
if (titleElement) { | |
const title = titleElement?.title || titleElement.textContent || '' | |
const link = titleElement.href | |
if (matchesCriteria(title)) { | |
jobStats.titleMatches++ | |
if (!processedJobUrls.has(link)) { | |
console.log(`Found matching job: ${title}`) | |
jobs.push({ link, title }) | |
} else { | |
console.log(`Skipping already processed job: ${title}`) | |
} | |
} | |
} | |
saveStats() | |
updateUI() | |
} | |
if (jobs.length === 0) { | |
console.log('No matching jobs found on this page, trying next page') | |
await goToNextPage() | |
return | |
} | |
console.log(`Found ${jobs.length} matching jobs to process`) | |
await processNextJob(jobs, 0) | |
} | |
// Process jobs one by one | |
async function processNextJob(jobs, index) { | |
if (stopRequested || index >= jobs.length) { | |
// If we've processed all jobs, go to next page | |
if (!stopRequested && index >= jobs.length) { | |
await goToNextPage() | |
} | |
return | |
} | |
const job = jobs[index] | |
console.log(`Processing job ${index + 1}/${jobs.length}: ${job.title}`) | |
// Mark as processed to avoid duplicates | |
processedJobUrls.add(job.link) | |
saveProcessedUrls() | |
// Navigate to job details | |
console.log(`Navigating to: ${job.link}`) | |
window.location.href = job.link | |
} | |
// Process job details page | |
async function processJobDetails(returnUrl) { | |
if (stopRequested) return | |
console.log('Processing job details page') | |
preventNewTabs() | |
const titleElement = await waitForElement('h1.job-title') | |
const descriptionElement = await waitForElement( | |
'span[itemprop="description"]' | |
) | |
if (!titleElement || !descriptionElement) { | |
console.error('Job title or description not found') | |
returnToSearchPage(returnUrl) | |
return | |
} | |
const jobDetails = { | |
title: titleElement.textContent || '', | |
description: descriptionElement.textContent || '' | |
} | |
if ( | |
matchesCriteria(jobDetails.title) && | |
matchesCriteria(jobDetails.description) | |
) { | |
jobStats.descriptionMatches++ | |
saveStats() | |
updateUI() | |
console.log('Job matches criteria, attempting to apply') | |
const respondButton = await waitForElement('.btn.btn-apply.btn-warning') | |
if (respondButton) { | |
respondButton.removeAttribute('target') | |
respondButton.click() | |
} else { | |
console.error('Respond button not found') | |
returnToSearchPage(returnUrl) | |
} | |
} else { | |
console.log('Job does not match full criteria') | |
returnToSearchPage(returnUrl) | |
} | |
} | |
// Process application form page | |
async function processApplicationForm(returnUrl) { | |
if (stopRequested) return | |
console.log('Processing application form') | |
const submitButton = await waitForElement( | |
'button.btn.btn-primary[type="submit"]' | |
) | |
if (submitButton) { | |
submitButton.click() | |
jobStats.applicationsSubmitted++ | |
saveStats() | |
updateUI() | |
console.log('Application submitted successfully') | |
showNotification('Application submitted!', 'success') | |
// Wait for application submission to complete | |
await waitForCondition( | |
() => !window.location.href.includes('/candidate/apply'), | |
10000 | |
) | |
returnToSearchPage(returnUrl) | |
} else { | |
console.error('Submit button not found') | |
showNotification('Failed to submit application.', 'error') | |
returnToSearchPage(returnUrl) | |
} | |
} | |
// Helper function to return to search page and continue processing | |
function returnToSearchPage(returnUrl) { | |
const lastSearchPage = GM_getValue('lastSearchPage', null) | |
if (returnUrl) { | |
console.log(`Returning to search page: ${returnUrl}`) | |
window.location.href = returnUrl | |
} else if (lastSearchPage) { | |
console.log(`Returning to last known search page: ${lastSearchPage}`) | |
window.location.href = lastSearchPage | |
} else { | |
console.error('No return URL provided and no last search page saved') | |
isProcessing = false | |
updateUI() | |
} | |
} | |
// Navigate to next page | |
async function goToNextPage() { | |
if (stopRequested) return | |
console.log('Looking for next page') | |
const nextLink = document.querySelector('.pagination .next a') | |
if (nextLink) { | |
console.log('Found next page link, clicking...') | |
nextLink.click() | |
} else { | |
console.log('No more pages to process') | |
stopRequested = true | |
isProcessing = false | |
updateUI() | |
showNotification('No more pages to process. Process complete!', 'info') | |
} | |
} | |
// Monitor for page changes to automatically continue workflow | |
function setupPageChangeMonitor() { | |
let lastUrl = window.location.href | |
// Clear any existing timer | |
if (pageChangeTimer) { | |
clearInterval(pageChangeTimer) | |
} | |
pageChangeTimer = setInterval(() => { | |
if (window.location.href !== lastUrl) { | |
console.log(`Page changed from ${lastUrl} to ${window.location.href}`) | |
lastUrl = window.location.href | |
// If we're still processing, determine the current page and continue | |
if (isProcessing && !stopRequested) { | |
// Give the page a moment to load | |
setTimeout(() => determinePage(), 1000) | |
} | |
} | |
}, 500) | |
} | |
// Main function | |
async function main() { | |
if (isProcessing) { | |
console.log('Already processing, ignoring start request') | |
return | |
} | |
isProcessing = true | |
stopRequested = false | |
updateUI() | |
showNotification('Workflow started.', 'success') | |
// Load saved processed URLs | |
loadProcessedUrls() | |
try { | |
// Set up page change monitoring | |
setupPageChangeMonitor() | |
// Start processing the current page | |
await determinePage() | |
} catch (error) { | |
console.error('Error in main function:', error) | |
showNotification('An error occurred: ' + error.message, 'error') | |
isProcessing = false | |
updateUI() | |
} | |
} | |
// Reset function | |
function resetStats() { | |
jobStats = { | |
jobsScanned: 0, | |
titleMatches: 0, | |
descriptionMatches: 0, | |
applicationsSubmitted: 0, | |
pagesProcessed: 0 | |
} | |
saveStats() | |
updateUI() | |
showNotification('Statistics reset.', 'info') | |
} | |
// Reset processed jobs | |
function resetProcessedJobs() { | |
processedJobUrls = new Set() | |
saveProcessedUrls() | |
showNotification('Processed jobs list cleared.', 'info') | |
} | |
// Stop processing | |
function stop() { | |
stopRequested = true | |
isProcessing = false | |
if (pageChangeTimer) { | |
clearInterval(pageChangeTimer) | |
pageChangeTimer = null | |
} | |
updateUI() | |
showNotification('Workflow stopped.', 'info') | |
} | |
// Notification system | |
function showNotification(message, type) { | |
const existing = document.querySelector('.job-auto-apply-notification') | |
if (existing) existing.remove() | |
const notification = document.createElement('div') | |
notification.className = `job-auto-apply-notification notification-${type}` | |
notification.textContent = message | |
Object.assign(notification.style, { | |
position: 'fixed', | |
top: '10px', | |
left: '50%', | |
transform: 'translateX(-50%)', | |
padding: '10px 20px', | |
borderRadius: '4px', | |
zIndex: '1000', | |
color: 'white' | |
}) | |
switch (type) { | |
case 'success': | |
notification.style.backgroundColor = '#4CAF50' | |
break | |
case 'error': | |
notification.style.backgroundColor = '#F44336' | |
break | |
case 'warning': | |
notification.style.backgroundColor = '#FF9800' | |
break | |
case 'info': | |
notification.style.backgroundColor = '#2196F3' | |
break | |
} | |
document.body.appendChild(notification) | |
setTimeout(() => notification.remove(), 3000) | |
} | |
// UI Creation | |
function createUI() { | |
GM_addStyle(` | |
.job-auto-apply-panel { | |
position: fixed; | |
top: 10px; | |
right: 10px; | |
width: 350px; | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.2); | |
z-index: 9999; | |
font-family: 'Segoe UI', Arial, sans-serif; | |
padding: 15px; | |
} | |
.job-auto-apply-panel h1 { | |
font-size: 18px; | |
margin: 0 0 10px; | |
color: #333; | |
} | |
.status-container { | |
display: flex; | |
align-items: center; | |
margin-bottom: 15px; | |
padding: 10px; | |
background: #f9f9f9; | |
border-radius: 4px; | |
} | |
.status-indicator { | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
margin-right: 5px; | |
} | |
.status-active { background: #4caf50; } | |
.status-inactive { background: #f44336; } | |
.button-container { | |
display: flex; | |
gap: 10px; | |
margin-bottom: 15px; | |
} | |
.job-auto-apply-panel button { | |
flex: 1; | |
padding: 8px; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-weight: 600; | |
transition: background 0.2s; | |
} | |
.job-auto-apply-panel button:hover { opacity: 0.9; } | |
.job-auto-apply-panel button:active { transform: translateY(1px); } | |
#startButton { background: #4caf50; color: white; } | |
#stopButton { background: #f44336; color: white; } | |
#settingsButton { background: #2196f3; color: white; } | |
.stats-container { | |
background: #f9f9f9; | |
border-radius: 4px; | |
padding: 10px; | |
margin-bottom: 15px; | |
} | |
.stats-title { | |
font-weight: 600; | |
margin-bottom: 8px; | |
} | |
.stat-item { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 5px; | |
} | |
.stat-value { font-weight: 600; } | |
.settings-panel { | |
display: none; | |
margin-top: 15px; | |
} | |
.settings-panel textarea { | |
width: 100%; | |
height: 80px; | |
margin-bottom: 10px; | |
border-radius: 4px; | |
border: 1px solid #ddd; | |
padding: 5px; | |
} | |
.settings-panel label { | |
display: block; | |
margin-bottom: 5px; | |
font-weight: 500; | |
} | |
.auto-start-checkbox { | |
margin: 10px 0; | |
} | |
.advanced-container { | |
margin-top: 10px; | |
} | |
.advanced-button-container { | |
display: flex; | |
gap: 10px; | |
margin-top: 10px; | |
} | |
#resetStatsButton { background: #ff9800; color: white; } | |
#resetJobsButton { background: #9c27b0; color: white; } | |
`) | |
const panel = document.createElement('div') | |
panel.className = 'job-auto-apply-panel' | |
panel.innerHTML = ` | |
<h1>Job Application Assistant</h1> | |
<div class="status-container"> | |
<div id="statusIndicator" class="status-indicator status-inactive"></div> | |
<div id="statusText">Workflow is not running</div> | |
</div> | |
<div class="button-container"> | |
<button id="startButton">Start</button> | |
<button id="stopButton">Stop</button> | |
<button id="settingsButton">Settings</button> | |
</div> | |
<div class="stats-container"> | |
<div class="stats-title">Statistics</div> | |
<div class="stat-item"><div>Jobs Scanned:</div><div id="jobsScanned" class="stat-value">0</div></div> | |
<div class="stat-item"><div>Title Matches:</div><div id="titleMatches" class="stat-value">0</div></div> | |
<div class="stat-item"><div>Description Matches:</div><div id="descriptionMatches" class="stat-value">0</div></div> | |
<div class="stat-item"><div>Applications Submitted:</div><div id="applicationsSubmitted" class="stat-value">0</div></div> | |
<div class="stat-item"><div>Pages Processed:</div><div id="pagesProcessed" class="stat-value">0</div></div> | |
</div> | |
<div class="advanced-container"> | |
<div class="advanced-button-container"> | |
<button id="resetStatsButton">Reset Stats</button> | |
<button id="resetJobsButton">Clear Job History</button> | |
</div> | |
</div> | |
<div class="settings-panel" id="settingsPanel"> | |
<label for="positiveKeywords">Positive Keywords:</label> | |
<textarea id="positiveKeywords"></textarea> | |
<label for="negativeKeywords">Negative Keywords:</label> | |
<textarea id="negativeKeywords"></textarea> | |
<div class="auto-start-checkbox"> | |
<input type="checkbox" id="autoStart"> <label for="autoStart">Auto-start on page load</label> | |
</div> | |
<div class="button-container"> | |
<button id="saveSettings">Save</button> | |
<button id="cancelSettings">Cancel</button> | |
</div> | |
</div> | |
` | |
document.body.appendChild(panel) | |
// Event listeners | |
document | |
?.getElementById('startButton') | |
?.addEventListener('click', () => main()) | |
document | |
?.getElementById('stopButton') | |
?.addEventListener('click', () => stop()) | |
document | |
?.getElementById('resetStatsButton') | |
?.addEventListener('click', () => resetStats()) | |
document | |
?.getElementById('resetJobsButton') | |
?.addEventListener('click', () => resetProcessedJobs()) | |
document | |
?.getElementById('settingsButton') | |
?.addEventListener('click', () => { | |
const settingsPanel = document.getElementById('settingsPanel') | |
settingsPanel.style.display = | |
settingsPanel?.style.display === 'block' ? 'none' : 'block' | |
if (settingsPanel?.style.display === 'block') { | |
document.getElementById('positiveKeywords').value = GM_getValue( | |
'positiveKeywords', | |
defaultSettings.positiveKeywords | |
) | |
document.getElementById('negativeKeywords').value = GM_getValue( | |
'negativeKeywords', | |
defaultSettings.negativeKeywords | |
) | |
document.getElementById('autoStart').checked = GM_getValue( | |
'autoStart', | |
defaultSettings.autoStart | |
) | |
} | |
}) | |
document.getElementById('saveSettings')?.addEventListener('click', () => { | |
const settings = { | |
positiveKeywords: document | |
.getElementById('positiveKeywords') | |
?.value.trim(), | |
negativeKeywords: document | |
.getElementById('negativeKeywords') | |
?.value.trim(), | |
autoStart: document.getElementById('autoStart')?.checked | |
} | |
if (!settings.positiveKeywords) { | |
showNotification( | |
'Please enter at least one positive keyword.', | |
'warning' | |
) | |
return | |
} | |
saveSettings(settings) | |
document.getElementById('settingsPanel').style.display = 'none' | |
showNotification('Settings saved!', 'success') | |
}) | |
document.getElementById('cancelSettings')?.addEventListener('click', () => { | |
document.getElementById('settingsPanel').style.display = 'none' | |
}) | |
} | |
// Update UI | |
function updateUI() { | |
document.getElementById('statusIndicator').className = `status-indicator ${ | |
isProcessing ? 'status-active' : 'status-inactive' | |
}` | |
document.getElementById('statusText').textContent = isProcessing | |
? 'Workflow is running' | |
: 'Workflow is not running' | |
document.getElementById('jobsScanned').textContent = jobStats.jobsScanned | |
document.getElementById('titleMatches').textContent = jobStats.titleMatches | |
document.getElementById('descriptionMatches').textContent = | |
jobStats.descriptionMatches | |
document.getElementById('applicationsSubmitted').textContent = | |
jobStats.applicationsSubmitted | |
document.getElementById('pagesProcessed').textContent = | |
jobStats.pagesProcessed | |
} | |
// Initialize | |
function initialize() { | |
loadStats() | |
loadProcessedUrls() | |
createUI() | |
updateUI() | |
setupPageChangeMonitor() | |
const settings = loadSettings() | |
if (settings.autoStart && !isProcessing) { | |
// Small delay to ensure page is fully loaded | |
setTimeout(() => main(), 1500) | |
} | |
} | |
initialize() | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A Tampermonkey userscript that automates the process of applying to frontend web developer jobs on the Joblum job board. The script scans job listings, filters them based on user-defined keywords, and automatically submits applications for matching jobs.
Features
Keyword-Based Filtering: Automatically identifies relevant jobs using customizable positive (e.g., frontend, react) and negative (e.g., native, PHP) keywords.
Automated Navigation: Processes job search results, job details, and application form pages, navigating between them seamlessly.
Progress Tracking: Tracks and displays statistics, including jobs scanned, title matches, description matches, applications submitted, and pages processed.
Persistent State: Saves processed job URLs and user settings to avoid duplicate applications and maintain preferences across sessions.
User Interface: Provides a control panel with start/stop controls, settings management, and real-time statistics.
Notifications: Displays success, error, warning, and info notifications for user feedback.
Auto-Start Option: Optionally starts the automation process automatically when visiting the job board.
Reset Capabilities: Allows resetting statistics and clearing the history of processed jobs.
Requirements
Tampermonkey: Install the Tampermonkey browser extension for Chrome, Firefox, or another supported browser.
Browser: A modern web browser compatible with Tampermonkey (e.g., Chrome, Firefox, Edge).
Installation
Install the Tampermonkey extension from your browser's extension store.
Click the Tampermonkey icon in your browser and select Create a new script.
Copy and paste the entire script code into the Tampermonkey editor.
Save the script (Ctrl+S or File > Save).
Visit ru.joblum.com to use the script.
Usage
Access the Job Board: Navigate to ru.joblum.com.
Control Panel: A panel appears in the top-right corner of the page with the following options:
Start: Begins the automation process, scanning and applying to matching jobs.
Stop: Halts the automation process.
Settings: Opens a settings panel to configure positive/negative keywords and auto-start.
Reset Stats: Clears the statistics displayed in the panel.
Clear Job History: Resets the list of processed jobs to allow re-processing.
Settings Configuration:
Positive Keywords: Enter comma-separated keywords for desired jobs (e.g., frontend, react, javascript).
Negative Keywords: Enter keywords to exclude jobs (e.g., native, PHP, mobile).
Auto-Start: Check to enable automatic processing on page load.
Save or cancel changes using the respective buttons.
Monitor Progress: The panel displays real-time statistics and a status indicator (green for running, red for stopped).
Notifications: Temporary notifications appear at the top of the page to indicate actions or errors.
Notes