From c655f2e0784d99516c71adf0eda61aec18114994 Mon Sep 17 00:00:00 2001 From: Bastian Gruber Date: Thu, 29 Jan 2026 16:24:44 +0000 Subject: [PATCH] Update filters and add cleanup --- config.yaml | 4 - dashboard.py | 551 +++-- data/dashboard.html | 5242 ++++++++++++++++++++++++++++++++++--------- db.py | 23 + docker-compose.yaml | 1 + main.py | 7 + notify.py | 108 +- 7 files changed, 4694 insertions(+), 1242 deletions(-) diff --git a/config.yaml b/config.yaml index e7a412a..e5a878a 100644 --- a/config.yaml +++ b/config.yaml @@ -72,10 +72,6 @@ companies: platform: greenhouse board_token: automatticcareers - - name: Canonical - platform: greenhouse - board_token: canonical - - name: ClickHouse platform: greenhouse board_token: clickhouse diff --git a/dashboard.py b/dashboard.py index cbe77d2..aa809ba 100644 --- a/dashboard.py +++ b/dashboard.py @@ -3,168 +3,193 @@ Generate a simple text-based HTML dashboard of all tracked jobs. """ -import re from datetime import datetime from pathlib import Path +from collections import Counter from db import Database -# Regions/locations we care about (case-insensitive matching) -DESIRED_REGIONS = [ - "canada", "toronto", "vancouver", - "germany", "berlin", "munich", - "emea", - "americas", # includes North/South America - "north america", - "worldwide", "global", "anywhere", -] - -# Locations to explicitly exclude (on-site or remote restricted to these) -EXCLUDED_LOCATIONS = [ - # US cities/states (we don't want US-only jobs) - "san francisco", "new york", "nyc", "seattle", "austin", "boston", - "chicago", "denver", "los angeles", "atlanta", "dallas", "houston", - "california", "washington", "texas", "massachusetts", "colorado", - "united states", "usa", "u.s.", "us-", "usa-", +# Location grouping rules: keyword -> (group_id, display_name) +# Order matters - first match wins +LOCATION_RULES = [ + # Canada + (["canada", "toronto", "vancouver", "montreal", "ottawa", "calgary", "waterloo"], "canada", "Canada"), + # Germany + (["germany", "berlin", "munich", "frankfurt", "hamburg"], "germany", "Germany"), # UK - "london", "united kingdom", "uk", "dublin", "ireland", - # Australia/APAC (not EMEA) - "sydney", "melbourne", "australia", "singapore", "tokyo", "japan", - "india", "bangalore", "bengaluru", "hyderabad", "delhi", - "korea", "seoul", "taiwan", "taipei", "china", "beijing", "shenzhen", - # Other excluded - "israel", "tel aviv", "brazil", "sao paulo", "mexico", - "netherlands", "amsterdam", "france", "paris", "spain", "madrid", - "portugal", "lisbon", "poland", "warsaw", "italy", - "czech", "prague", "serbia", "belgrade", "cyprus", "limassol", - "austria", "vienna", "sweden", "stockholm", "denmark", "copenhagen", - "switzerland", "romania", "bucharest", "hungary", "greece", - "south africa", "indonesia", "jakarta", "malaysia", + (["united kingdom", " uk", "uk ", "london", "england", "manchester", "edinburgh"], "uk", "UK"), + # Ireland + (["ireland", "dublin"], "ireland", "Ireland"), + # Netherlands + (["netherlands", "amsterdam", "rotterdam"], "netherlands", "Netherlands"), + # France + (["france", "paris"], "france", "France"), + # Spain + (["spain", "madrid", "barcelona"], "spain", "Spain"), + # Poland + (["poland", "warsaw", "krakow", "wroclaw"], "poland", "Poland"), + # Sweden + (["sweden", "stockholm"], "sweden", "Sweden"), + # Switzerland + (["switzerland", "zurich", "geneva"], "switzerland", "Switzerland"), + # Australia + (["australia", "sydney", "melbourne"], "australia", "Australia"), + # India + (["india", "bangalore", "bengaluru", "hyderabad", "delhi", "mumbai", "pune"], "india", "India"), + # Japan + (["japan", "tokyo"], "japan", "Japan"), + # Singapore + (["singapore"], "singapore", "Singapore"), + # Israel + (["israel", "tel aviv"], "israel", "Israel"), + # Brazil + (["brazil", "sao paulo"], "brazil", "Brazil"), + # US (must be after other countries to avoid false matches) + (["united states", "usa", "u.s.", "san francisco", "new york", "nyc", "seattle", + "austin", "boston", "chicago", "denver", "los angeles", "atlanta", "dallas", + "houston", "california", "washington", "texas", "massachusetts", "colorado", + "portland", "miami", "phoenix", "san diego", "san jose", "palo alto", + "mountain view", "sunnyvale", "menlo park", "cupertino"], "us", "US"), + # Regions + (["emea"], "emea", "EMEA"), + (["americas", "north america", "latam"], "americas", "Americas"), + (["apac", "asia pacific", "asia-pacific"], "apac", "APAC"), + (["worldwide", "global", "anywhere", "earth"], "worldwide", "Worldwide"), ] -def is_location_relevant(location: str, remote_type: str) -> bool: +def extract_location_info(location: str, remote_type: str) -> tuple[list[str], str]: """ - Strict location filter. Only keeps jobs available in Canada, Germany, EMEA, or Worldwide. - Filters out US-only jobs, UK jobs, APAC jobs, etc. + Extract location tags and short display text from a job's location. + Returns (list of tag ids, short display location) """ - if not location: - return False # No location info = probably US-based, filter out - - loc_lower = location.lower() - - # Check if any desired region is mentioned FIRST - has_desired = any(region in loc_lower for region in DESIRED_REGIONS) - - # If it has a desired region, keep it (even if it also mentions excluded locations) - # e.g., "Remote (United States | Canada)" should be kept because of Canada - if has_desired: - return True - - # If it just says "Remote" with nothing else, keep it (truly remote) - if loc_lower.strip() == "remote": - return True - - # Check for excluded locations - has_excluded = any(excl in loc_lower for excl in EXCLUDED_LOCATIONS) - if has_excluded: - return False - - # Check for patterns like "In-Office", "Hybrid", "On-site" without desired region - if any(x in loc_lower for x in ["in-office", "hybrid", "on-site", "onsite", "office based"]): - return False - - # If we can't determine, filter it out (safer) - return False - - -def extract_location_tags(location: str, remote_type: str) -> tuple[list[str], str]: - """ - Extract relevant location tags and a short display location. - Returns (list of tag names, short location string) - """ - if not location: - return [], "" - - loc_lower = location.lower() tags = [] - short_loc = "" + display = "" + + if not location: + return tags, display + + loc_lower = location.lower() # Check for remote is_remote = remote_type == "remote" or "remote" in loc_lower if is_remote: tags.append("remote") - # Check for Canada - if any(x in loc_lower for x in ["canada", "toronto", "vancouver"]): - tags.append("canada") - short_loc = "Canada" + # Check against location rules + for keywords, tag_id, display_name in LOCATION_RULES: + if any(kw in loc_lower for kw in keywords): + if tag_id not in tags: + tags.append(tag_id) + if not display: + display = display_name - # Check for Germany/Berlin - if any(x in loc_lower for x in ["germany", "berlin", "munich"]): - tags.append("germany") - short_loc = "Germany" if "germany" in loc_lower else "Berlin" + # Fallback display + if not display: + if is_remote: + display = "Remote" + elif location: + display = location[:25] + "..." if len(location) > 25 else location - # Check for EMEA - if "emea" in loc_lower: - tags.append("emea") - short_loc = "EMEA" - - # Check for Americas/North America - if "americas" in loc_lower or "north america" in loc_lower: - tags.append("americas") - short_loc = "Americas" - - # Check for Worldwide - if any(x in loc_lower for x in ["worldwide", "global", "anywhere"]): - tags.append("worldwide") - short_loc = "Worldwide" - - # If no specific region found but it's remote - if not short_loc and is_remote: - short_loc = "Remote" - - return tags, short_loc + return tags, display def generate_dashboard(output_path: str = "data/dashboard.html"): """Generate a static HTML dashboard.""" db = Database() jobs = db.get_all_active_jobs() - - # Get all monitored companies all_company_names = db.get_all_companies() - # Track total jobs per company (before location filtering) - total_per_company = {} - for company_name, job in jobs: - total_per_company[company_name] = total_per_company.get(company_name, 0) + 1 - - # Group by company, filtering out irrelevant remote locations + # Process all jobs and collect location data companies = {} - filtered_count = 0 + location_counts = Counter() + for company_name, job in jobs: - if not is_location_relevant(job.location, job.remote_type): - filtered_count += 1 - continue + # Extract location info + tags, display = extract_location_info(job.location, job.remote_type) + + # Count locations for filter generation + for tag in tags: + location_counts[tag] += 1 + + # Store processed job data if company_name not in companies: companies[company_name] = [] - companies[company_name].append(job) - # Ensure all monitored companies are in the dict (even with 0 jobs) + companies[company_name].append({ + "job": job, + "tags": tags, + "display": display, + "search_text": f"{job.title.lower()} {(job.location or '').lower()} {(job.department or '').lower()} {' '.join(tags)}" + }) + + # Ensure all companies exist (even with 0 jobs) for name in all_company_names: if name not in companies: companies[name] = [] - if name not in total_per_company: - total_per_company[name] = 0 - total_shown = sum(len(jobs) for jobs in companies.values()) - total_scraped = sum(total_per_company.values()) - - # Sort companies by name + total_jobs = sum(len(j) for j in companies.values()) sorted_companies = sorted(companies.items()) + # Generate dynamic location filters (only show locations that exist in data) + # Order: Remote first, then by count descending + location_filters = [] + if "remote" in location_counts: + location_filters.append(("remote", "Remote", location_counts["remote"])) + + # Add other locations sorted by count + other_locations = [(tag, count) for tag, count in location_counts.items() if tag != "remote"] + other_locations.sort(key=lambda x: -x[1]) + + # Map tag_id to display name + tag_display = {tag_id: display for keywords, tag_id, display in LOCATION_RULES} + tag_display["remote"] = "Remote" + + for tag_id, count in other_locations: + display = tag_display.get(tag_id, tag_id.title()) + location_filters.append((tag_id, display, count)) + + # Generate location filter buttons HTML + location_buttons = "" + for tag_id, display, count in location_filters: + location_buttons += f' \n' + + # Generate tag colors dynamically + tag_colors = { + "remote": ("#1a4a1a", "#4ade80"), + "canada": ("#4a1a1a", "#f87171"), + "germany": ("#4a4a1a", "#facc15"), + "uk": ("#2a1a3a", "#a78bfa"), + "us": ("#3a2a1a", "#fb923c"), + "emea": ("#1a3a4a", "#60a5fa"), + "americas": ("#3a1a4a", "#c084fc"), + "worldwide": ("#1a4a3a", "#34d399"), + "apac": ("#1a2a4a", "#38bdf8"), + "ireland": ("#1a4a2a", "#4ade80"), + "netherlands": ("#3a3a1a", "#fbbf24"), + "france": ("#2a2a4a", "#818cf8"), + "spain": ("#4a2a1a", "#fb7185"), + "poland": ("#3a1a2a", "#f472b6"), + "sweden": ("#1a3a3a", "#2dd4bf"), + "switzerland": ("#4a1a2a", "#fb7185"), + "australia": ("#2a3a1a", "#a3e635"), + "india": ("#4a3a1a", "#fcd34d"), + "japan": ("#4a1a3a", "#e879f9"), + "singapore": ("#1a4a4a", "#22d3d1"), + "israel": ("#3a2a2a", "#fca5a5"), + "brazil": ("#2a4a1a", "#86efac"), + } + + # Generate CSS for tags + tag_css = "" + for tag_id, (bg, fg) in tag_colors.items(): + tag_css += f""" .tag-{tag_id} {{ + background: {bg}; + color: {fg}; + }} +""" + html = f""" @@ -299,30 +324,7 @@ def generate_dashboard(output_path: str = "data/dashboard.html"): font-size: 11px; margin-left: 5px; }} - .tag-remote {{ - background: #1a4a1a; - color: #4ade80; - }} - .tag-canada {{ - background: #4a1a1a; - color: #f87171; - }} - .tag-berlin {{ - background: #4a4a1a; - color: #facc15; - }} - .tag-emea {{ - background: #1a3a4a; - color: #60a5fa; - }} - .tag-americas {{ - background: #3a1a4a; - color: #c084fc; - }} - .tag-worldwide {{ - background: #1a4a3a; - color: #34d399; - }} +{tag_css} .hidden {{ display: none; }} @@ -342,26 +344,32 @@ def generate_dashboard(output_path: str = "data/dashboard.html"): flex-wrap: wrap; gap: 10px; }} - .toc-links a {{ + .toc-link {{ color: var(--accent); text-decoration: none; font-size: 13px; }} - .toc-links a:hover {{ + .toc-link:hover {{ text-decoration: underline; }} - .toc-links .empty {{ + .toc-link.empty {{ color: var(--muted); - cursor: default; }} - .toc-links .empty:hover {{ - text-decoration: none; + .toc-link.hidden {{ + display: none; }} - .filter-buttons {{ + .filter-section {{ display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; + align-items: center; + }} + .filter-label {{ + color: var(--muted); + font-size: 12px; + margin-right: 4px; + min-width: 60px; }} .filter-btn {{ background: var(--bg); @@ -383,6 +391,13 @@ def generate_dashboard(output_path: str = "data/dashboard.html"): border-color: var(--accent); color: var(--bg); }} + .clear-btn {{ + border-color: #666; + }} + .clear-btn:hover {{ + border-color: #f87171; + color: #f87171; + }} @@ -390,47 +405,47 @@ def generate_dashboard(output_path: str = "data/dashboard.html"):

$ job-board

Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | - {total_shown}/{total_scraped} jobs (location filtered) | Monitoring {len(all_company_names)} companies + {total_jobs} jobs | {len(all_company_names)} companies
- -
- - - - - - - - - - - - - - + +
+ Quick: + + +
+
+ Location: +{location_buttons}
+
+ Role: + + + + + + + +
- {total_shown} jobs shown + {total_jobs} jobs shown
Jump to company:
- @@ -438,44 +453,34 @@ def generate_dashboard(output_path: str = "data/dashboard.html"):
""" - # Job listings (only for companies with jobs) + # Job listings for company_name, company_jobs in sorted_companies: if not company_jobs: - continue # Skip companies with no jobs after filtering - anchor = company_name.lower().replace(" ", "-") + continue + anchor = company_name.lower().replace(" ", "-").replace("'", "") + total = len(company_jobs) html += f""" -
+
{company_name} - {len(company_jobs)} positions + {total} positions
""" - for job in sorted(company_jobs, key=lambda j: j.title): - location = job.location or "" - location_lower = location.lower() - - # Extract tags and short location - tag_list, short_loc = extract_location_tags(location, job.remote_type) + for job_data in sorted(company_jobs, key=lambda j: j["job"].title): + job = job_data["job"] + tags = job_data["tags"] + display = job_data["display"] + search_text = job_data["search_text"] # Build tag HTML - tags = "" - if "remote" in tag_list: - tags += 'remote' - if "canada" in tag_list: - tags += 'canada' - if "germany" in tag_list: - tags += 'germany' - if "emea" in tag_list: - tags += 'emea' - if "americas" in tag_list: - tags += 'americas' - if "worldwide" in tag_list: - tags += 'worldwide' + tag_html = "" + for tag in tags: + tag_html += f'{tag}' - html += f"""
- {job.title}{tags} - {short_loc} + html += f"""
+ {job.title}{tag_html} + {display}
""" html += """
@@ -488,67 +493,155 @@ def generate_dashboard(output_path: str = "data/dashboard.html"): const search = document.getElementById('search'); const jobs = document.querySelectorAll('.job'); const companies = document.querySelectorAll('.company'); + const tocLinks = document.querySelectorAll('.toc-link'); const visibleCount = document.getElementById('visible-count'); const filterBtns = document.querySelectorAll('.filter-btn'); + const clearBtn = document.querySelector('.clear-btn'); - function filterJobs(query) { - let visible = 0; - const terms = query.toLowerCase().trim().split(/\\s+/).filter(t => t); + // Track active filters by category + const activeFilters = { + location: null, + role: null + }; + + function applyFilters() { + let totalVisible = 0; + const searchTerms = search.value.toLowerCase().trim().split(/\\s+/).filter(t => t); + + // Build filter terms from active category filters + const locationTerms = activeFilters.location ? activeFilters.location.split(/\\s+/) : []; + const roleTerms = activeFilters.role ? activeFilters.role.split(/\\s+/) : []; + + const hasFilters = searchTerms.length > 0 || locationTerms.length > 0 || roleTerms.length > 0; + + // Track visible counts per company + const companyCounts = {}; companies.forEach(company => { + const companyId = company.dataset.company; const companyJobs = company.querySelectorAll('.job'); let companyVisible = 0; companyJobs.forEach(job => { const searchText = job.dataset.search; - // Match if ANY term matches (OR logic for filter buttons) - const matches = terms.length === 0 || terms.some(term => searchText.includes(term)); + + // Match logic: AND between categories, OR within each category + let matches = true; + + // Search box (OR within terms) + if (searchTerms.length > 0) { + matches = matches && searchTerms.some(term => searchText.includes(term)); + } + + // Location filter (OR within terms) + if (locationTerms.length > 0) { + matches = matches && locationTerms.some(term => searchText.includes(term)); + } + + // Role filter (OR within terms) + if (roleTerms.length > 0) { + matches = matches && roleTerms.some(term => searchText.includes(term)); + } + job.classList.toggle('hidden', !matches); if (matches) { companyVisible++; - visible++; + totalVisible++; } }); company.classList.toggle('hidden', companyVisible === 0); + companyCounts[companyId] = companyVisible; + + // Update company header count + const countSpan = company.querySelector('.company-count'); + const total = parseInt(countSpan.dataset.total); + if (!hasFilters) { + countSpan.textContent = `${total} positions`; + } else { + countSpan.textContent = `${companyVisible}/${total} positions`; + } }); - visibleCount.textContent = `${visible} jobs shown`; + // Update TOC links - always show all, grey out empty ones + tocLinks.forEach(link => { + const companyId = link.dataset.company; + const total = parseInt(link.dataset.total); + const visible = companyCounts[companyId] || 0; + const name = link.textContent.replace(/\\s*\\(.*\\)/, ''); + + if (!hasFilters) { + link.textContent = `${name} (${total})`; + link.classList.toggle('empty', total === 0); + } else { + link.textContent = `${name} (${visible}/${total})`; + link.classList.toggle('empty', visible === 0); + } + // Always show the link, never hide + link.classList.remove('hidden'); + }); + + visibleCount.textContent = `${totalVisible} jobs shown`; } - search.addEventListener('input', (e) => { - // Clear active button when typing + function clearAllFilters() { + search.value = ''; + activeFilters.location = null; + activeFilters.role = null; filterBtns.forEach(btn => btn.classList.remove('active')); - filterJobs(e.target.value); + applyFilters(); + } + + search.addEventListener('input', () => { + applyFilters(); }); - // Filter buttons filterBtns.forEach(btn => { btn.addEventListener('click', () => { const filter = btn.dataset.filter; - search.value = filter; - filterBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - filterJobs(filter); + const category = btn.dataset.category; + const action = btn.dataset.action; + + // Handle clear button + if (action === 'clear') { + clearAllFilters(); + return; + } + + // Handle "All" button + if (category === 'all') { + clearAllFilters(); + return; + } + + // Toggle filter in category + const categoryBtns = document.querySelectorAll(`.filter-btn[data-category="${category}"]`); + + if (btn.classList.contains('active')) { + // Deselect + btn.classList.remove('active'); + activeFilters[category] = null; + } else { + // Select (deselect others in same category) + categoryBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + activeFilters[category] = filter; + } + + applyFilters(); }); }); - // Keyboard shortcut: / to focus search document.addEventListener('keydown', (e) => { if (e.key === '/' && document.activeElement !== search) { e.preventDefault(); search.focus(); } if (e.key === 'Escape') { - search.value = ''; - filterBtns.forEach(b => b.classList.remove('active')); - filterJobs(''); + clearAllFilters(); search.blur(); } }); - - // Set "All" as active by default - filterBtns[0].classList.add('active'); diff --git a/data/dashboard.html b/data/dashboard.html index eb6d900..b06bfca 100644 --- a/data/dashboard.html +++ b/data/dashboard.html @@ -140,10 +140,18 @@ background: #4a1a1a; color: #f87171; } - .tag-berlin { + .tag-germany { background: #4a4a1a; color: #facc15; } + .tag-uk { + background: #2a1a3a; + color: #a78bfa; + } + .tag-us { + background: #3a2a1a; + color: #fb923c; + } .tag-emea { background: #1a3a4a; color: #60a5fa; @@ -156,6 +164,63 @@ background: #1a4a3a; color: #34d399; } + .tag-apac { + background: #1a2a4a; + color: #38bdf8; + } + .tag-ireland { + background: #1a4a2a; + color: #4ade80; + } + .tag-netherlands { + background: #3a3a1a; + color: #fbbf24; + } + .tag-france { + background: #2a2a4a; + color: #818cf8; + } + .tag-spain { + background: #4a2a1a; + color: #fb7185; + } + .tag-poland { + background: #3a1a2a; + color: #f472b6; + } + .tag-sweden { + background: #1a3a3a; + color: #2dd4bf; + } + .tag-switzerland { + background: #4a1a2a; + color: #fb7185; + } + .tag-australia { + background: #2a3a1a; + color: #a3e635; + } + .tag-india { + background: #4a3a1a; + color: #fcd34d; + } + .tag-japan { + background: #4a1a3a; + color: #e879f9; + } + .tag-singapore { + background: #1a4a4a; + color: #22d3d1; + } + .tag-israel { + background: #3a2a2a; + color: #fca5a5; + } + .tag-brazil { + background: #2a4a1a; + color: #86efac; + } + .hidden { display: none; } @@ -175,26 +240,32 @@ flex-wrap: wrap; gap: 10px; } - .toc-links a { + .toc-link { color: var(--accent); text-decoration: none; font-size: 13px; } - .toc-links a:hover { + .toc-link:hover { text-decoration: underline; } - .toc-links .empty { + .toc-link.empty { color: var(--muted); - cursor: default; } - .toc-links .empty:hover { - text-decoration: none; + .toc-link.hidden { + display: none; } - .filter-buttons { + .filter-section { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; + align-items: center; + } + .filter-label { + color: var(--muted); + font-size: 12px; + margin-right: 4px; + min-width: 60px; } .filter-btn { background: var(--bg); @@ -216,175 +287,238 @@ border-color: var(--accent); color: var(--bg); } + .clear-btn { + border-color: #666; + } + .clear-btn:hover { + border-color: #f87171; + color: #f87171; + }

$ job-board

- Last updated: 2026-01-20 13:24:03 | - 421 jobs | Monitoring 28 companies + Last updated: 2026-01-29 16:23:39 | + 1171 jobs | 27 companies
- -
- - - - - - - - - - - - - - + +
+ Quick: + + +
+
+ Location: + + + + + + + + + + + + + + + + + + + + + + +
+
+ Role: + + + + + + + +
- 421 jobs shown + 1171 jobs shown
Jump to company:
-
-
+
1Password - 21 positions + 26 positions
-
- Build Engineer remotecanada +
+ Build Engineer remotecanadaus Canada
-
- Developer, Backendremotecanada +
+ Developer, Backendremotecanadaus Canada
-
- Developer, Enterprise Readyremotecanada +
+ Developer, Enterprise Readyremotecanadaus Canada
-
- Director, Product Management - Platformsremotecanada + -
- Director, Security Researchremotecanada +
+ Junior Rust Developerremotenetherlands + Netherlands +
+
+ Manager, Security Incident Responseremotecanadaus Canada
-
- Engineering Manager, Product Engineeringremotecanada + -
- Manager, Security Incident Responseremotecanada + -
- Principal Developer, AI & Developer Teamremotecanada +
+ Privacy Engineerremotecanadaus Canada
-
- Principal Product Marketing Manager, Developerremotecanada +
+ Rust Software Developer, Security for AIremoteuknetherlands + UK +
+
+ Security Engineer, Corporate Securityremotecanadaus Canada
-
- Security Engineer, Corporate Securityremotecanada +
+ Senior Developer, Data Securityremotecanadaus Canada
-
- Senior Developer, Data Securityremotecanada +
+ Senior Developer, Railsremotecanadaus Canada
-
- Senior Director, Developer Platformremotecanada + +
+ Senior Director, Developer Platformremotecanadaus Canada
-
- Senior IT Engineerremotecanada +
+ Senior IT Engineerremotecanadaus Canada
-
- Senior Security Engineer, Application Securityremotecanada +
+ Senior Privacy Engineerremotecanadaus Canada
-
- Senior Security Engineer, Detection and Responseremotecanada + -
- Senior Security Engineer, GRC Automationremotecanada + -
- Senior Security Engineer, Threat Intelligenceremotecanada + -
- Solutions Engineer, Commercialremotecanada + -
- Staff Developer, Authentication Enablementremotecanada + +
+ Solutions Engineer, Commercialremotecanadaus Canada
- -
+
Automattic - 6 positions + 9 positions
+
+ Applied AI Engineerremote + Remote +
+ +
Experienced Software Engineerremote Remote @@ -412,790 +546,2701 @@
-
+
- Canonical - 141 positions + Bitwarden + 2 positions
-
- Alliances Field Engineerworldwide - Worldwide +
+ QA Engineerremoteus + US
-
- Associate Linux Support Engineerworldwide - Worldwide -
- - -
- Cloud Engineering Managerworldwide - Worldwide -
-
- Cloud Field Engineerworldwide - Worldwide -
-
- Cloud Field Engineering Managerworldwide - Worldwide -
-
- Cloud Support Engineerworldwide - Worldwide -
- - - - - - - - -
- Engineering Managerworldwide - Worldwide -
-
- Engineering Manager - App Storesworldwide - Worldwide -
-
- Engineering Manager - AppArmorworldwide - Worldwide -
- -
- Engineering Manager - Data Platformworldwide - Worldwide -
-
- Engineering Manager - MLOps & Analyticsworldwide - Worldwide -
- - - -
- Engineering Manager - Solutions Engineeringemeaamericas - Americas -
-
- Engineering Manager - Ubuntu Coreemeaamericas - Americas -
-
- Engineering Manager - Ubuntu Securityworldwide - Worldwide -
-
- Engineering Manager - Webemea - EMEA -
- - -
- Engineering Manager, Managed Servicesworldwide - Worldwide -
- - -
- Golang Engineerworldwide - Worldwide -
- - - -
- HPC Software Engineeremeaamericas - Americas -
-
- Head of Security Operationsworldwide - Worldwide -
-
- IoT Data Engineeremea - EMEA -
-
- Juju Software Engineer (Go)worldwide - Worldwide -
-
- Junior Cloud Field Engineerworldwide - Worldwide -
-
- Junior Data Engineeremea - EMEA -
-
- Junior Linux Kernel Engineer - Ubuntuworldwide - Worldwide -
- -
- Junior Ubuntu Software Engineerworldwide - Worldwide -
- - - -
- Lead Linux Kernel Engineer - Ubuntuworldwide - Worldwide -
- -
- Linux Cryptography and Security Engineerworldwide - Worldwide -
-
- Linux Desktop & Devices Support Engineerworldwide - Worldwide -
-
- Linux Devices Software Engineerworldwide - Worldwide -
- -
- Linux Kernel Engineerworldwide - Worldwide -
- - -
- Linux devices software engineer - snapdemeaamericas - Americas -
- - -
- MLOps Field Engineerworldwide - Worldwide -
-
- Microservices Engineerworldwide - Worldwide -
-
- Observability Engineering Manageremeaamericas - Americas -
- -
- OpenStack Engineering Managerworldwide - Worldwide -
-
- Performance Engineer - Open Sourceworldwide - Worldwide -
- -
- Python Engineerworldwide - Worldwide -
- - - - - - -
- Security Risk Management Specialistworldwide - Worldwide -
-
- Security Software Engineerworldwide - Worldwide -
- -
- Senior Growth Engineeremea - EMEA -
-
- Senior Juju Software Engineer (Go)worldwide - Worldwide -
-
- Senior Security Operations Engineerworldwide - Worldwide -
- -
- Senior Site Reliability Engineerworldwide - Worldwide -
- -
- Senior Software Engineer - MAASemeaamericas - Americas -
- - -
- Senior Web Engineeremea - EMEA -
-
- Senior/Staff/Principal Engineerworldwide - Worldwide -
-
- Site Reliability / Gitops Engineerworldwide - Worldwide -
-
- Site Reliability Engineerworldwide - Worldwide -
- - -
- Software Developer (Backend SaaS)americas - Americas -
-
- Software Engineer (Python/Linux/Packaging)emeaamericas - Americas -
-
- Software Engineer - App Storesworldwide - Worldwide -
-
- Software Engineer - Cloud Imagesamericas - Americas -
- - - - -
- Software Engineer - L3 Supportworldwide - Worldwide -
-
- Software Engineer - OpenStackemeaamericas - Americas -
- - -
- Software Engineer - Python and K8sworldwide - Worldwide -
- - - - - - -
- Software Engineering Directorworldwide - Worldwide -
- - - - -
- Software Maintenance Engineerworldwide - Worldwide -
- -
- Software Support Engineerworldwide - Worldwide -
-
- Staff Engineer, Development Lifecycleemeaamericas - Americas -
-
- Staff Security Operations Engineerworldwide - Worldwide -
- -
- Support Engineering Managerworldwide - Worldwide -
-
- Sustaining Operations Engineerworldwide - Worldwide -
- - - - - -
- Ubuntu Engineering Leadworldwide - Worldwide -
-
- Ubuntu Engineering Managerworldwide - Worldwide -
- - -
- Ubuntu Sales Engineer (Entry-Level)worldwide - Worldwide -
-
- Ubuntu Security Engineerworldwide - Worldwide -
-
- Ubuntu Software Engineerworldwide - Worldwide -
- -
- Web Developeremea - EMEA -
-
-
+
ClickHouse - 22 positions + 110 positions
+ + +
+ Core Software Engineer (C++) - Remoteremotenetherlands + Netherlands +
+
+ Core Software Engineer (C++) - Remoteremoteindia + India +
+ + +
+ Curriculum Developer & Instructor - APJremoteaustralia + Australia +
+
+ Database Reliability Engineer - Core Teamremotenetherlands + Netherlands +
+ +
+ Database Reliability Engineer - Core Teamremoteaustralia + Australia +
+
+ Engineering Manager - Chinaremote + Remote +
+
+ Engineering Manager - Database Integrationsremotenetherlands + Netherlands +
+ + + +
+ Full Stack Software Engineer - Billing Teamremotenetherlands + Netherlands +
+ + +
+ Full Stack Software Engineer - Control Planeremotenetherlands + Netherlands +
+ + +
+ Incident Response Security Engineerremotenetherlands + Netherlands +
+ + + + +
+ Principal Product Manager, Securityremotenetherlands + Netherlands +
+ +
Product Security Engineerremotecanada Canada
+
+ Product Security Engineerremotenetherlands + Netherlands +
+
+ Product Security Engineerremoteus + US +
+
+ QA Engineer - Core Database (remote)remotespain + Spain +
+
+ QA Engineer - Core Database (remote)remoteindia + India +
+ + +
+ QA Engineer - Core Database (remote)remotenetherlands + Netherlands +
+ -
- Senior Cloud Engineer - Product Metricsremotecanada + +
+ Senior Backend Engineer - Data Ingestion (ClickPipes)remotenetherlands + Netherlands +
+ + + +
+ Senior Cloud Engineer remoteus + US +
+ +
+ Senior Consulting Engineer - APJremoteaustralia + Australia +
+
+ Senior Consulting Engineer - APJremotesingapore + Singapore +
+ +
+ Senior Consulting Engineer - EMEAremotefrance + France +
+
+ Senior Consulting Engineer - EMEAremotenetherlands + Netherlands +
+ + +
+ Senior Developer Relations Advocate - EMEAnetherlands + Netherlands +
+
+ Senior Full Stack Engineer - HyperDXremotenetherlands + Netherlands +
+ + + + + + + + + + + + + +
+ Senior Software Engineer (Infrastructure) - HyperDXremotenetherlands + Netherlands +
+ +
+ Senior Software Engineer (TypeScript) - AI/MLremotenetherlands + Netherlands +
+ + + +
+ Senior Software Engineer - Cloud Infrastructureremotesingapore + Singapore +
+ +
+ Senior Software Engineer - Cloud Infrastructureremoteaustralia + Australia +
+ +
+ Senior Software Engineer - Cloud Infrastructureremotenetherlands + Netherlands +
+ + + + +
+ Senior Software Engineer - Postgresremoteindia + India +
-
-
+
- Datadog - 2 positions + Cloudflare + 298 positions
+ + +
+ AI Engineer + Hybrid +
+
+ Application Security Engineer + Hybrid; In-Office +
+
+ Application Security Engineer + Distributed; Hybrid +
+ + + + + + + + + + + + +
+ Design Engineer, Radar + Hybrid +
+ + + +
+ Developer Solutions Strategist + Distributed +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Hardware Systems Engineer + In-Office +
+ +
+ IAM Security Engineer + Hybrid; In-Office +
+ + + + + + + + + + +
+ Network Deployment Engineer + In-Office +
+ + + +
+ Network Engineer + Hybrid +
+ + +
+ Partner Solutions Engineer, UK&I + Hybrid; In-Office +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Senior Data Engineer + In-Office +
+
+ Senior Design Engineer + Hybrid +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Senior Software Engineer - Backend + Hybrid; In-Office +
+ + + + + + + + + + + + + + + + + + + +
+ Senior Solutions Engineer + Distributed +
+
+ Senior Solutions Engineer + Distributed +
+
+ Senior Solutions Engineer + Distributed +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Software Engineer + Hybrid +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Solutions Engineer + Distributed +
+
+ Solutions Engineer + Distributed +
+ + + + + + +
+ Solutions Engineer, Zero Trust + Distributed +
+ + + + + + + +
+ Systems Engineer + Hybrid +
+ + + + +
+ Systems Engineer, Data + Hybrid +
+ +
+ Systems Engineer, FL + Hybrid +
+
+ Systems Engineer, Frontend + In-Office +
+
+ Systems Engineer, Fullstack + In-Office +
+ + + + + + +
+ Technical Support Engineer + Hybrid; In-Office +
+ + + +
+ VP, Developer Adoption + Hybrid +
+ + +
+
+ +
+
+ CockroachLabs + 3 positions +
+
+ +
+ Sales Engineeruk + UK +
+
+ Staff Sales Engineer, Southremoteus + US +
+
+
+ +
+
+ Datadog + 160 positions +
+
+ + + + + + +
+ Commercial Sales Engineernetherlands + Netherlands +
+ +
+ Commercial Sales Engineer (Portuguese-speaking)irelandnetherlands + Ireland +
+ + + + + + + + +
+ Enterprise Sales Engineerjapan + Japan +
+
+ Enterprise Sales Engineerremoteus + US +
+
+ Enterprise Sales Engineerindia + India +
+
+ Enterprise Sales Engineerjapan + Japan +
+ + + + + + + +
+ Enterprise Sales Engineer - Spainremotespain + Spain +
+ + + + + + + + + + + + +
Manager I, Developer Advocacyremotecanada Canada
-
- Senior Software Engineer โ€“ IDE Integrations (VS Code & Cursor)remotegermany + + + + + + + + + + + + + + + + + + + + +
+ Manager Sales Engineer (EMEA) ireland + Ireland +
+ + + + + + +
+ Mid-Market Sales Engineer (Spanish-speaking)irelandnetherlands + Ireland +
+ + + +
+ Product Designer II - Securityfrancespain + France +
+ +
+ Regional Manager, Sales Engineering + Jakarta, Indonesia +
+ + + +
+ Sales Engineer - Majors (UK)remoteuk + UK +
+ + + + + + + + + +
+ Senior Engineer - Linuxisrael + Israel +
+ +
+ Senior Sales Engineerremoteus + US +
+ + + + + + +
+ Senior Security Researcher - GenAIfrancespain + France +
+
+ Senior Security Researcher - GenAI + Lisbon, Portugal +
+
+ Senior Software Engineerfrancespain + France +
+ + + + + + + + + + + + +
+ Senior Software Engineer - Backend & Scalabilityfrancespainisrael + France +
+ +
+ Senior Software Engineer - Data Sciencefrancespain + France +
+
+ Senior Software Engineer - Distributed Systemsfrancespainisrael + France +
+
+ Senior Software Engineer - Frontendfrancespain + France +
+ + + + +
+ Senior Software Engineer โ€“ IDE Integrations (VS Code & Cursor)remotegermanyukirelandnetherlandsfrancespainpolandswedenswitzerland Germany
+ + + + + +
+ Staff AI Engineer - Notebooks + Lisbon, Portugal +
+
+ Staff AI Engineer - Notebooks francespain + France +
+ + +
+ Staff Software Engineer francespain + France +
+ + + + + +
+ Staff Software Engineer - RUM Platformfrancespain + France +
+ + + + +
+ Technical Escalations Engineer 2 (APM) - EMEAirelandnetherlandsfrance + Ireland +
+ + +
+ Technical Support Engineer 1japan + Japan +
+
+ Technical Support Engineer 1japan + Japan +
+
+ Technical Support Engineer 1 - 2 + Seoul, South Korea +
+
+ Technical Support Engineer 1 - 2 + Seoul, South Korea +
+ +
+ Technical Support Engineer 2 + Seoul, South Korea +
+
+ Technical Support Engineer 2japan + Japan +
+
+ Technical Support Engineer 2japan + Japan +
+ +
+ Technical Support Engineer 2, Premieraustralia + Australia +
+ +
+ Technical Support Engineer 2, Premier - EMEAirelandnetherlandsfrance + Ireland +
+
-
+
- Dropbox - 34 positions + Discord + 40 positions
+
+ Data Engineering Managerremoteus + US +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Staff Data Engineer, Adsremoteus + US +
+ + + + + + + + + +
+
+ +
+
+ Dropbox + 76 positions +
+
+ +
CX Software Engineerremotecanada Canada
-
- Engineering Manager, DocSendremotecanada + + +
+ Frontend Product Software Engineerremotepoland + Poland +
+
+ Full Stack Product Software Engineerremotepoland + Poland +
+ -
- Fullstack Product Software Engineer, Core Web Experienceremotecanada - Canada + + -
- Infrastructure Software Engineer, Metadata remotecanada + + +
+ Infrastructure Software Engineerremotecanada Canada
+ + + +
+ Machine Learning Engineer, Dashremote + Remote +
+
+ Network Engineerremote + Remote +
+ -
- Principal Software Engineerremotecanada - Canada -
-
- Principal Software Engineer, Developer Productivityremotecanada - Canada + -
- Product Backend Software Engineer, Search Platformremotecanada - Canada + -
- Senior Android Software Engineer, Mobile Experienceremotecanada - Canada + -
- Senior Backend Product Software Engineer, Reclaimremotecanada - Canada + + + + + + - + + + + -
- Senior Machine Learning Engineer, Dash remotecanada - Canada + +
Senior Network Engineer, Corporate ITremotecanada @@ -1205,30 +3250,86 @@ Senior Salesforce Developer, Managed Storefrontremotecanada Canada
+ + +
+ ServiceNow Engineerremote + Remote +
+
+ ServiceNow Engineerremotepoland + Poland +
+
+ Site Reliability Engineerremote + Remote +
+ +
+ Software Engineer Intern (Summer 2026)remotepoland + Poland +
+ + -
- Staff Backend Product Software Engineer, Core Syncremotecanada + - -
+
DuckDuckGo - 4 positions + 3 positions
- -
+ + +
GitLab - 45 positions + 48 positions
@@ -1287,6 +3485,10 @@ Associate Support Engineer (EMEA)remoteemea EMEA
+
+ CX Platform Engineer - AMERremotecanada + Canada +
Developer Relations Engineerremoteemea EMEA @@ -1301,19 +3503,23 @@
Engineering Manager, Fulfillment remotecanadaamericas - Americas + Canada
Engineering Manager, Gitlab Deliveryremotecanadaemea - EMEA + Canada +
+
Engineering Manager, Infrastructure Platformsremoteemeaamericas - Americas + EMEA
-
+ -
- Intermediate Backend Engineer (Ruby on Rails), Analytics Instrumentation remotecanadaemea - EMEA + -
- Intermediate Fullstack Engineer (TypeScript), AI Engineering: Editor Extensions โ€“ Multi-Platformremotecanada +
@@ -1345,7 +3551,7 @@
Intermediate Support Engineer (AMER - PST / MST)remotecanada @@ -1355,28 +3561,28 @@ Manager, Customer Success Engineersremotecanada Canada
-
- Practice Engineer - AMERremotecanada - Canada -
Principal Database Engineer, Data Engineeringremoteemeaamericas - Americas + EMEA
-
- Principal Engineer, Software Supply Chain Securityremotecanada +
+ Principal Engineer, Software Supply Chain Securityremotecanadauknetherlandsisrael Canada
-
- Principal Infrastructure Security Architectremotecanadaemea - EMEA + -
- Principal Product Manager, Security & Complianceremotecanada +
+ Principal Infrastructure Security Engineerremotecanadaemeaapac + Canada +
+
@@ -1387,41 +3593,49 @@ Principal Product Marketing Manager, DevSecOps Platformremotecanada Canada
-
- Principal Security Engineer, Application Security remoteemeaamericas - Americas +
Senior Backend (Go) Engineer, Gitlab Delivery -Operateremoteemeaamericas - Americas + EMEA
-
- Senior Backend Engineer (Ruby on Rails), Verify: Pipeline Executionremotecanada + + +
Senior Backend Engineer (Ruby), Tenant Scale, Georemoteemeaamericas - Americas + EMEA
-
- Senior Backend Engineer(Golang),Software Supply Chain Security: Auth Infrastructureremotecanadaamericas - Americas +
Senior Frontend Engineer, AI Engineering: Duo Chatremoteemeaamericas - Americas + EMEA
-
- Senior Fullstack Engineer (RoR/vue.js), Software Supply Chain Security: Authorizationremotecanada + -
- Senior Infrastructure Security Engineer remoteemea - EMEA +
+ Senior Infrastructure Security Engineer remoteaustraliajapanemea + Australia
Senior Manager, Developer Advocacyremoteemeaamericas - Americas + EMEA
Senior PSIRT Security Engineer, EMEAremoteemea @@ -1429,369 +3643,558 @@
Senior Site Reliability Engineer, Environment Automationremotecanadaamericas - Americas + Canada
-
- Staff Backend Engineer, Developer Experience (Ruby)remotecanadaemea - EMEA +
+ Staff Backend Engineer, Developer Experience (Ruby)remotecanadauknetherlandsemeaapac + Canada
Staff Engineer, GitLab Delivery - Operateremoteemeaamericas - Americas -
-
- Staff Frontend Engineer (Vue.js), Plan:Knowledgeremoteamericas - Americas + EMEA
-
- Staff Salesforce Engineer, CRM Systemsremoteamericas - Americas +
+ Zuora Engineerremoteapac + APAC
-
+
GrafanaLabs - 12 positions + 37 positions
+
+ Commercial Solutions Engineer | Benelux | Remoteremotenetherlands + Netherlands +
-
- Senior AI Engineer - Grafana Ops, AI/ML | Canada | Remoteremotecanada - Canada + -
- Senior AI Engineer, GenAI & ML Evaluation Frameworks - Grafana Ops, AI/ML | Canada | Remote remotecanada - Canada + + -
- Senior Backend Software Engineer, Alerting | Canada | Remoteremotecanada - Canada + + + + + + + + + + + + + + + + + + + - -
+
Honeycomb - 2 positions + 2 positions
+ -
- Staff AI Engineerremotecanada - Canada -
-
+
JetBrains - 58 positions + 65 positions
-
- (Senior) Backend Developer (Java/Kotlin) - Business Application Developmentremotegermany + -
- Engineering Squad Lead (JetBrains Research)remotegermany + -
- FullStack ML Developerremotegermany +
+ Engineering Squad Lead (JetBrains Research)remotegermanynetherlandspoland Germany
-
- Head of Corporate Securitygermany + +
+ FullStack ML Developerremotegermanyuknetherlandspoland Germany
-
- Infrastructure Security Engineerremotegermany +
+ Head of Corporate Securitygermanynetherlands Germany
-
- Java Developer (Backend) โ€’ TeamCity, Build Tools Integrationgermany +
+ Infrastructure Security Engineerremotegermanynetherlandspoland Germany
-
- JetBrains Go Developer Advocate (Developer Advocacy)remotegermany + -
- JetBrains Rust Developer Advocategermany +
+ JetBrains Go Developer Advocate (Developer Advocacy)remotegermanynetherlandspoland Germany
-
- JetBrains Web Developer Advocate (Developer Advocacy)remotegermany +
+ JetBrains Rust Developer Advocategermanyuknetherlandsus + Germany +
+
+ JetBrains Web Developer Advocate (Developer Advocacy)remotegermanynetherlandsspainpoland Germany
-
- Kotlin Developer Advocategermany +
+ Kotlin Developer Advocategermanyuknetherlandsspain Germany
-
- Machine Learning Evaluation Engineer (Agentic Mobile App Generator)germany + -
- Project Maintainer โ€“ DPAI Arena Evaluation Infrastructureremotegermany + -
- Project Manager (Software Engineering Research)germany +
+ Project Maintainer โ€“ DPAI Arena Evaluation Infrastructureremotegermanynetherlandspoland Germany
-
- QA Engineer (Kotlin Build Tools)germany +
+ QA Engineer (Kotlin Build Tools)germanypoland Germany
-
- QA Engineer (Kotlin Compiler Frontend)germany +
+ QA Engineer (Kotlin Compiler Frontend)remotegermanypoland Germany
-
- QA Engineer (Rider IDE)germany +
+ QA Engineer (Rider IDE)germanypoland Germany
-
- Quality Engineer (IDEs department) germany + +
+ Research Engineer (Agentic Models)remotegermanyuknetherlandspoland Germany
-
- Research Engineer (Agentic Models)remotegermany +
+ Research Engineer (LLM Training and Performance)germanyuknetherlandspoland Germany
-
- Research Engineer (LLM Training and Performance)germany +
+ Research Engineer - JetBrains AIremotegermanyuknetherlandspoland Germany
-
- Research Engineer - JetBrains AIremotegermany + -
- SDET Engineer in KED QA Automation teamgermany + -
- SDET Engineer in Kotlin Performance QA teamgermany +
+ SSH & Remote Development Engineergermanypoland Germany
-
- SSH & Remote Development Engineergermany +
+ Security Compliance Specialistremotegermanynetherlandspoland Germany
-
- Security Compliance Specialistremotegermany - Germany -
-
- Security Engineer in Product Securityremotegermany +
+ Security Engineer in Product Securityremotegermanynetherlandspoland Germany
-
- Senior Developer at Rider (GameDev Tools) remotegermany +
+ Senior Developer at Rider (GameDev Tools) remotegermanypoland Germany
-
- Senior ML Engineer (JetBrains Research)remotegermany +
+ Senior ML Engineer (JetBrains Research)remotegermanynetherlandspoland Germany
-
- Senior Machine Learning Engineer (IntelliJ AI)germany +
+ Senior Machine Learning Engineer (IntelliJ AI)germanyuknetherlands Germany
-
- Senior Product Manager (AI BI Platform)germany +
+ Senior Product Manager (AI BI Platform)germanyuknetherlandspoland Germany
-
- Senior Product Manager (IntelliJ Platform)germany +
+ Senior Product Manager (IntelliJ Platform)germanynetherlandspoland Germany
-
- Senior QA engineer (BAD)remotegermany +
+ Senior QA engineer (BAD)remotegermanypoland Germany
-
- Senior Software Developer (IntelliJ AI)germany + -
- Senior Software Developer (Qodana Core)remotegermany +
+ Senior Software Developer (Qodana Core)remotegermanypoland Germany
-
- Senior Software Developer (Quality Infrastructure) germany + -
- Senior Software Developer (Rider)remotegermany +
+ Senior Software Developer (Rider)remotegermanypoland Germany
-
- Senior Software Developer - Kotlin Nativeremotegermany + -
- Senior Software Engineer (.NET tooling Core)remotegermany + -
- Senior Software Engineer โ€“ IntelliJ Ultimate Teamgermany +
+ Senior Software Engineer (.NET tooling Core)remotegermanypoland Germany
-
- Senior/Staff Software Developer - Kotlin Multiplatform Toolingremotegermany + -
- Software Developer (IntelliJ Platform โ€“ Version Control Experience)germany + -
- Software Developer (IntelliJ Platform)remotegermany + -
- Software Developer (Orca)germany + -
- Software Developer (Platform/ Remote Development)germany +
+ Software Developer (IntelliJ Platform)remotegermanypoland Germany
-
- Software Developer (Station/Toolbox App)germany +
+ Software Developer (Ktor Framework)germanypoland Germany
-
- Software Development Engineer in Test (Rider)germany +
+ Software Developer (Ktor Framework)remotegermany Germany
-
- Software Engineer (IntelliJ Platfrom Licensing)germany +
+ Software Developer (Orca)germanypoland Germany
-
- Staff Product Manager (High Performance Data Infrastructure)germany + -
- Staff Software Developer (Kotlin Libraries)remotegermany + -
- Support Engineer (Business Applications Development)remotegermany + -
- Support Engineer (IDE Services)remotegermany +
+ Software Developer for Bonsai (Innovation Hub)remotegermanypoland Germany
-
- Support Engineer (JetBrains Academy)germany + -
- Support Engineer (JetBrains Console)germany + +
+ Staff Product Manager (High Performance Data Infrastructure)germanyuknetherlandspolandisrael + Germany +
+
+ Staff Software Developer (Kotlin Libraries)remotegermanyuknetherlandspoland + Germany +
+
+ Support Engineer (Business Applications Development)remotegermanypoland + Germany +
+
+ Support Engineer (IDE Services)remotegermanypoland + Germany +
+
+ Support Engineer (JetBrains Academy)germanypoland Germany
-
- Technical Lead (IntelliJ Platform)germany +
+ Technical Lead (IntelliJ Platform)germanynetherlandspoland Germany
-
-
+
+
+ Materialize + 4 positions +
+ +
+ +
PingCAP - 1 positions + 7 positions
-
+
Railway - 2 positions + 6 positions
+
+ Developer Relationsremoteus + US +
+
+ Infra Engineer - Datacentersremoteus + US +
+
+ Infrastructure Engineerremoteus + US +
+
Senior Platform Engineer: Storageremoteworldwide Worldwide @@ -1803,10 +4206,51 @@
-
+
+
+ Render + 8 positions +
+
+ +
+ Product Lead, Infrastructureremoteus + US +
+
+ Software Engineer, Billingremoteus + US +
+ +
+ Software Engineer, Productremoteus + US +
+
+ Software Engineer, Securityremoteus + US +
+
+ Technical Content Engineerremoteus + US +
+ +
+
+ +
Rerun - 4 positions + 5 positions
@@ -1821,6 +4265,10 @@ Full-stack Engineer - Customer Experienceremote Remote
+
+ Robotics ML Engineerremote + Remote +
Software Engineer (Rust) - Backend remote Remote @@ -1828,20 +4276,48 @@
-
+
Sentry - 7 positions + 36 positions
+ + + +
+ Head of Securityus + US +
+
+ Sales Engineerus + US +
+ +
Senior Full Stack Engineer, Core Product canada Canada @@ -1850,71 +4326,351 @@ Senior Fullstack Engineer, Data Browsingcanada Canada
+ + + +
+ Senior Software Engineer (iOS), SDK + Vienna, Austria +
+ + + + + + + + + + -
-
+
Stripe - 28 positions + 158 positions
-
- Account Executive, Platforms (German-speaking)germany - Berlin + -
- Backend Engineer, DEePremotecanada + +
+ Account Executive, Platformsireland + Ireland +
+ + + + + + + + + + +
+ Android Engineer, Terminalcanada + Canada +
+ +
+ Android Engineer, Terminal OS Platformremotecanadaus + Canada +
+ + +
+ Backend Engineer, Core Technologyireland + Ireland +
+
+ Backend Engineer, Core Technology + Bucharest, Romania +
+
+ Backend Engineer, DEePremotecanadaus Canada
Backend Engineer, Datacanada Canada
-
- Backend Engineer, Link canada - Canada + + + + + -
- Client Onboarding Integration Engineercanada +
+ Broadcast Engineerus + US +
+ + + +
+ Demo Engineer us + US +
+ + + + +
+ Engineering Manager - Dashboardireland + Ireland +
+ +
+ Engineering Manager, Disputes Foundationsingapore + Singapore +
+ + + + + + +
+ Enterprise Integration Engineer + Mexico City, MX +
+ + + + + + +
Full Stack Engineer, Enterprise & Ecosystemcanada Canada @@ -1923,10 +4679,62 @@ Full Stack Engineer, Linkremotecanada Canada
+ + + + + + + + +
+ IT Support Engineersingapore + Singapore +
+
+ Integration Engineer (Japan)japan + Japan +
+ + +
Integration Reliability Engineer, Technical Operations, Cardsremotecanada Canada @@ -1943,26 +4751,274 @@ Launch Integration Engineercanada Canada
-
- Security Engineer, New Grad canada + + + + + + + + + + + + + +
+ SaaS Operations Engineerireland + Ireland +
+ + + + + + +
+ Security Engineer, New Grad canadaus Canada
+ + + +
+ Software Engineer, Cardssingapore + Singapore +
+ +
+ Software Engineer, Data & AIindia + India +
+ +
+ Software Engineer, Internireland + Ireland +
+ +
+ Software Engineer, Internsingapore + Singapore +
+ + + +
+ Software Engineer, Money as a Servicesingapore + Singapore +
+
+ Software Engineer, New Gradireland + Ireland +
+
+ Software Engineer, New Gradspain + Spain +
+ + + + + +
+ Software Engineering, New Gradsingapore + Singapore +
+ + + + + + +
+ Staff Engineer , Data & AIindia + India +
+ + + + + + + + + + + + +
Staff Security Engineer, Security Partnershipsremoteamericas Americas @@ -1975,21 +5031,49 @@ Staff Software Engineer, Dataremotecanada Canada
-
-
+
Supabase - 19 positions + 18 positions
@@ -2040,10 +5124,6 @@ Postgres Engineerremote Remote
-
- Rust Engineerremote - Remote -
Software Engineer: Platform Servicesremote Remote @@ -2071,23 +5151,39 @@
-
+
Tailscale - 12 positions + 24 positions
+
+ Analytics Engineer, Dataremoteus + US +
Analytics Engineer, Dataremotecanada Canada
+
+ Backend Engineer, Control Planeremotecanada + Canada +
+ +
+ Backend Engineer, Identityremoteus + US +
Backend Engineer, Identityremotecanada Canada
-
- Backend Engineer, Platformremotecanada - Canada +
Manager, Solutions Engineering - Enterpriseremotecanada @@ -2097,10 +5193,22 @@ Product Growth Engineerremotecanada Canada
+
+ Product Growth Engineerremoteus + US +
+ +
+ Security Software Engineerremoteus + US +
Security Software Engineerremotecanada Canada @@ -2109,10 +5217,22 @@ Software Engineer, Networkingremotecanada Canada
+ + +
Solutions Engineer - Commercial (Expansion Sales)remotecanada Canada @@ -2121,6 +5241,14 @@ Solutions Engineer - Commercial (New Business)remotecanada Canada
+ +
+ Test Automation Engineerremoteus + US +
Test Automation Engineerremotecanada Canada @@ -2128,12 +5256,40 @@
-
+
TigerData - 1 positions + 8 positions
+ +
+ Customer Delivery Engineer - EMEAremotespain + Spain +
+ + + + +
Senior Software Engineer, AI Tools โ€“ Tiger Labsremote Remote @@ -2146,67 +5302,155 @@ const search = document.getElementById('search'); const jobs = document.querySelectorAll('.job'); const companies = document.querySelectorAll('.company'); + const tocLinks = document.querySelectorAll('.toc-link'); const visibleCount = document.getElementById('visible-count'); const filterBtns = document.querySelectorAll('.filter-btn'); + const clearBtn = document.querySelector('.clear-btn'); - function filterJobs(query) { - let visible = 0; - const terms = query.toLowerCase().trim().split(/\s+/).filter(t => t); + // Track active filters by category + const activeFilters = { + location: null, + role: null + }; + + function applyFilters() { + let totalVisible = 0; + const searchTerms = search.value.toLowerCase().trim().split(/\s+/).filter(t => t); + + // Build filter terms from active category filters + const locationTerms = activeFilters.location ? activeFilters.location.split(/\s+/) : []; + const roleTerms = activeFilters.role ? activeFilters.role.split(/\s+/) : []; + + const hasFilters = searchTerms.length > 0 || locationTerms.length > 0 || roleTerms.length > 0; + + // Track visible counts per company + const companyCounts = {}; companies.forEach(company => { + const companyId = company.dataset.company; const companyJobs = company.querySelectorAll('.job'); let companyVisible = 0; companyJobs.forEach(job => { const searchText = job.dataset.search; - // Match if ANY term matches (OR logic for filter buttons) - const matches = terms.length === 0 || terms.some(term => searchText.includes(term)); + + // Match logic: AND between categories, OR within each category + let matches = true; + + // Search box (OR within terms) + if (searchTerms.length > 0) { + matches = matches && searchTerms.some(term => searchText.includes(term)); + } + + // Location filter (OR within terms) + if (locationTerms.length > 0) { + matches = matches && locationTerms.some(term => searchText.includes(term)); + } + + // Role filter (OR within terms) + if (roleTerms.length > 0) { + matches = matches && roleTerms.some(term => searchText.includes(term)); + } + job.classList.toggle('hidden', !matches); if (matches) { companyVisible++; - visible++; + totalVisible++; } }); company.classList.toggle('hidden', companyVisible === 0); + companyCounts[companyId] = companyVisible; + + // Update company header count + const countSpan = company.querySelector('.company-count'); + const total = parseInt(countSpan.dataset.total); + if (!hasFilters) { + countSpan.textContent = `${total} positions`; + } else { + countSpan.textContent = `${companyVisible}/${total} positions`; + } }); - visibleCount.textContent = `${visible} jobs shown`; + // Update TOC links - always show all, grey out empty ones + tocLinks.forEach(link => { + const companyId = link.dataset.company; + const total = parseInt(link.dataset.total); + const visible = companyCounts[companyId] || 0; + const name = link.textContent.replace(/\s*\(.*\)/, ''); + + if (!hasFilters) { + link.textContent = `${name} (${total})`; + link.classList.toggle('empty', total === 0); + } else { + link.textContent = `${name} (${visible}/${total})`; + link.classList.toggle('empty', visible === 0); + } + // Always show the link, never hide + link.classList.remove('hidden'); + }); + + visibleCount.textContent = `${totalVisible} jobs shown`; } - search.addEventListener('input', (e) => { - // Clear active button when typing + function clearAllFilters() { + search.value = ''; + activeFilters.location = null; + activeFilters.role = null; filterBtns.forEach(btn => btn.classList.remove('active')); - filterJobs(e.target.value); + applyFilters(); + } + + search.addEventListener('input', () => { + applyFilters(); }); - // Filter buttons filterBtns.forEach(btn => { btn.addEventListener('click', () => { const filter = btn.dataset.filter; - search.value = filter; - filterBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - filterJobs(filter); + const category = btn.dataset.category; + const action = btn.dataset.action; + + // Handle clear button + if (action === 'clear') { + clearAllFilters(); + return; + } + + // Handle "All" button + if (category === 'all') { + clearAllFilters(); + return; + } + + // Toggle filter in category + const categoryBtns = document.querySelectorAll(`.filter-btn[data-category="${category}"]`); + + if (btn.classList.contains('active')) { + // Deselect + btn.classList.remove('active'); + activeFilters[category] = null; + } else { + // Select (deselect others in same category) + categoryBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + activeFilters[category] = filter; + } + + applyFilters(); }); }); - // Keyboard shortcut: / to focus search document.addEventListener('keydown', (e) => { if (e.key === '/' && document.activeElement !== search) { e.preventDefault(); search.focus(); } if (e.key === 'Escape') { - search.value = ''; - filterBtns.forEach(b => b.classList.remove('active')); - filterJobs(''); + clearAllFilters(); search.blur(); } }); - - // Set "All" as active by default - filterBtns[0].classList.add('active'); diff --git a/db.py b/db.py index 1ffff33..d35861d 100644 --- a/db.py +++ b/db.py @@ -247,3 +247,26 @@ class Database: "SELECT name FROM companies WHERE active = TRUE ORDER BY name" ) return [row["name"] for row in cursor.fetchall()] + + def cleanup_removed_companies(self, active_company_names: list[str]) -> list[str]: + """ + Remove companies (and their jobs) that are no longer in the config. + Returns list of removed company names. + """ + with self._get_conn() as conn: + # Get companies in DB but not in config + placeholders = ",".join("?" * len(active_company_names)) + cursor = conn.execute( + f"SELECT id, name FROM companies WHERE name NOT IN ({placeholders})", + active_company_names + ) + removed = [] + for row in cursor.fetchall(): + company_id = row["id"] + company_name = row["name"] + # Delete jobs first (foreign key) + conn.execute("DELETE FROM jobs WHERE company_id = ?", (company_id,)) + # Delete company + conn.execute("DELETE FROM companies WHERE id = ?", (company_id,)) + removed.append(company_name) + return removed diff --git a/docker-compose.yaml b/docker-compose.yaml index 09d9400..44fe030 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,7 @@ services: - /home/gruberb/.msmtprc:/root/.msmtprc:ro environment: - TZ=America/Toronto + - PYTHONUNBUFFERED=1 command: ["python", "main.py", "--schedule"] restart: unless-stopped logging: diff --git a/main.py b/main.py index ef6a1be..b910a04 100644 --- a/main.py +++ b/main.py @@ -145,6 +145,13 @@ def run_scraper(config: dict): notifier = Notifier(config.get("notifications", {})) companies = config.get("companies", []) + + # Cleanup companies no longer in config + active_names = [c["name"] for c in companies] + removed = db.cleanup_removed_companies(active_names) + if removed: + print(f"\n๐Ÿงน Removed {len(removed)} companies no longer in config: {', '.join(removed)}") + print(f"\nMonitoring {len(companies)} companies...") reports = [] diff --git a/notify.py b/notify.py index 201dc86..1718825 100644 --- a/notify.py +++ b/notify.py @@ -28,24 +28,23 @@ class Notifier: if not reports_with_changes: print("\nโœ“ No changes detected across all companies.") - return + else: + # Console output for changes + self._notify_console(reports_with_changes) - # Console output (always) - self._notify_console(reports_with_changes) - - # Email (if configured) + # Email (if configured) - only sends when there are changes email_config = self.config.get("email") - if email_config: + if email_config and reports_with_changes: self._notify_email(reports_with_changes, email_config) - # msmtp (if configured - uses system msmtp config) + # msmtp (if configured - sends daily summary always) msmtp_config = self.config.get("msmtp") if msmtp_config: - self._notify_msmtp(reports_with_changes, msmtp_config) + self._notify_msmtp_daily_summary(reports, msmtp_config) - # Slack (if configured) + # Slack (if configured) - only sends when there are changes slack_config = self.config.get("slack") - if slack_config: + if slack_config and reports_with_changes: self._notify_slack(reports_with_changes, slack_config) def _notify_console(self, reports: list[ChangeReport]): @@ -180,6 +179,95 @@ Content-Type: text/plain; charset=UTF-8 except Exception as e: print(f"โœ— Failed to send msmtp notification: {e}") + def _notify_msmtp_daily_summary(self, reports: list[ChangeReport], config: dict): + """Send daily summary email via system msmtp (always sends).""" + import subprocess + from datetime import datetime + + to_addr = config.get("to_addr", "me@bastiangruber.ca") + from_addr = config.get("from_addr", "admin@novanexus.ca") + + # Calculate totals + total_companies = len([r for r in reports if r.total_active > 0]) + total_jobs = sum(r.total_active for r in reports) + total_new = sum(len(r.new_jobs) for r in reports) + total_removed = sum(len(r.removed_jobs) for r in reports) + + # Build subject line + if total_new or total_removed: + changes = [] + if total_new: + changes.append(f"+{total_new}") + if total_removed: + changes.append(f"-{total_removed}") + subject = f"Job Board: {', '.join(changes)} | {total_jobs} jobs" + else: + subject = f"Job Board: No changes | {total_jobs} jobs" + + # Build plain text body + body_lines = [ + "JOB BOARD DAILY SUMMARY", + f"{datetime.now().strftime('%Y-%m-%d %H:%M')}", + "", + "OVERVIEW", + f" Companies with jobs: {total_companies}", + f" Total jobs tracked: {total_jobs}", + "", + ] + + # Changes section + reports_with_changes = [r for r in reports if r.new_jobs or r.removed_jobs] + + if reports_with_changes: + body_lines.append(f"CHANGES: +{total_new} new, -{total_removed} removed") + body_lines.append("-" * 40) + + for report in reports_with_changes: + if report.new_jobs: + for job in report.new_jobs: + location_str = f" [{job.location}]" if job.location else "" + remote_str = " (Remote)" if job.remote_type == "remote" else "" + body_lines.append(f" + {report.company_name}: {job.title}{location_str}{remote_str}") + + if report.removed_jobs: + for job in report.removed_jobs: + body_lines.append(f" - {report.company_name}: {job.title}") + + body_lines.append("") + else: + body_lines.append("CHANGES: No changes detected") + body_lines.append("") + + body_lines.append("---") + body_lines.append("https://jobs.novanexus.ca") + + body = "\n".join(body_lines) + + # Build email message + email_msg = f"""Subject: {subject} +From: {from_addr} +To: {to_addr} +Content-Type: text/plain; charset=UTF-8 + +{body} +""" + + try: + result = subprocess.run( + ["msmtp", to_addr], + input=email_msg, + capture_output=True, + text=True, + ) + if result.returncode == 0: + print("โœ“ Daily summary email sent") + else: + print(f"โœ— msmtp failed: {result.stderr}") + except FileNotFoundError: + print("โœ— msmtp not found - install with: apt install msmtp") + except Exception as e: + print(f"โœ— Failed to send daily summary: {e}") + def _notify_slack(self, reports: list[ChangeReport], config: dict): """Send Slack notification.""" import httpx