diff --git a/panels/departure/worker.js b/panels/departure/worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..0210eab55f1fa46c1ed4ec69795aa523fe92060f
--- /dev/null
+++ b/panels/departure/worker.js
@@ -0,0 +1,164 @@
+importScripts('/js/helper/AJAX.js');
+importScripts('/js/helper/check.js');
+importScripts('/js/helper/AbstractWorker.js');
+
+class DepartureWorker extends AbstractWorker {
+	
+	constructor() {
+		super();
+		this.setSpeed(360000);
+	}
+	
+	processConfig(config) {
+		if (isObject(config) && isArrayNotEmpty(config['stops'])) {
+			config['stops_converted'] = [];
+			for (var i = 0; i < config['stops'].length; ++i) {
+				config['stops_converted'].push({url:'https://vrrf.finalrewind.org/'+config['stops'][i].split(':').join('/')+'.json',method:'GET',params:{frontend:'json'}});
+			}	
+		}
+		return config;
+	}
+	
+	tick(callback) {
+		var config = this.config;
+		if (isObject(config) && isArrayNotEmpty(config['stops_converted'])) {
+			AJAX.request(config['stops_converted']).then(function(results) {
+				if (results.length == 0) return;
+				var
+					reftime = new Date(),
+					result = {
+						vrrf_version: {
+							actual: '0.0.0.0',
+							expected: '0.0.0.0'
+						},
+						errors: {},
+						info: {},
+						lines: {},
+						raw: {}
+					},
+					rawLines = {};
+				reftime = calcDateValue(reftime.getFullYear(), reftime.getMonth()+1, reftime.getDate(), reftime.getHours(), reftime.getMinutes());
+				for (var i = 0; i < results.length; ++i) {
+					if (!results[i].success) continue;
+					var data = results[i].response;
+					if (!isObject(data)) data = JSON.parse(data);
+					var stopName = config['stops'][i].split('/').join(' - ');
+					result['vrrf_version']['actual'] = data['version'];
+					result['vrrf_version']['expected'] = '0.0.0.0';
+					result['errors'][stopName] = data['error'];
+					result['info'][stopName] = '';
+					result['raw'][stopName] = data['raw'];
+					for (var j = 0; j < data['raw'].length; ++j) {
+						var entry = data['raw'][j];
+						// filter
+						if (isObjectNotEmpty(config['filter']) && !passFilter(config['filter'], entry)) continue;
+						// interprete
+						var
+							schedDate = entry['sched_date'].split('.'),
+							schedTime = entry['sched_time'].split(':'),
+							deptime = calcDateValue(schedDate[2], schedDate[1], schedDate[0], schedTime[0], schedTime[1]);
+						var delay = parseInt(entry['delay']);
+						if (isNaN(delay)) delay = 0;
+						if (((deptime + delay) - reftime) > 0) {
+							var ident = /*entry['lineref']['operator']+'|'+entry['lineref']['type']+'|'+*/entry['line']+'|'+entry['lineref']['identifier']+'|'+entry['key'];
+							if (!isObjectNotEmpty(result['lines'][ident])) {
+								result['lines'][ident] = {
+									timeValue: deptime,
+									line: entry['line'],
+									destination: entry['destination'],
+									type: entry['type'],
+									stops: [{
+										timeValue: deptime,
+										time: entry['sched_time'],
+										delay: entry['delay'],
+										cancel: entry['is_cancelled'],
+										name: stopName,
+										info: entry['info']
+									}]
+								};
+							} else {
+								result['lines'][ident]['stops'].push({
+										timeValue: deptime,
+										time: entry['sched_time'],
+										delay: entry['delay'],
+										cancel: entry['is_cancelled'],
+										name: stopName,
+										info: entry['info']
+									});
+							}
+						}
+					}
+				}
+				result['lines'] = sortData(Object.values(result['lines']));
+				if (isNumber(config.max)) {
+					result['lines'] = result['lines'].slice(0, config.max);
+				}
+				callback(result);
+			});	
+		}
+	}
+	
+}
+
+function passFilter(filter, entry) {
+	if (isObjectNotEmpty(filter['bl'])) {
+		if (isArrayNotEmpty(filter['bl']['line'])
+		&&  arrayContains(filter['bl']['line'], entry['line'])) {
+			return false;
+		}
+		if (isArrayNotEmpty(filter['bl']['type'])
+		&&  arrayContains(filter['bl']['type'], entry['type'])) {
+			return false;
+		}
+		if (isArrayNotEmpty(filter['bl']['platform'])
+		&&  arrayContains(filter['bl']['platform'], entry['platform'])) {
+			return false;
+		}
+		if (isArrayNotEmpty(filter['bl']['destination'])
+		&&  arrayContains(filter['bl']['destination'], entry['destination'])) {
+			return false;
+		}
+	}
+	if (isObjectNotEmpty(filter['wl'])) {
+		if (isArrayNotEmpty(filter['wl']['line'])
+		&&  !arrayContains(filter['wl']['line'], entry['line'])) {
+				return false;
+		}
+		if (isArrayNotEmpty(filter['wl']['type'])
+		&&  !arrayContains(filter['wl']['type'], entry['type'])) {
+				return false;
+		}
+		if (isArrayNotEmpty(filter['wl']['platform'])
+		&&  !arrayContains(filter['wl']['platform'], entry['platform'])) {
+				return false;
+		}
+		if (isArrayNotEmpty(filter['wl']['destination'])
+		&&  !arrayContains(filter['wl']['destination'], entry['destination'])) {
+				return false;
+		}
+	}
+	return true;
+}
+
+function sortData(data) {
+	for (var i = 0; i < data.length; ++i) {
+		data[i]['stops'] = data[i]['stops'].sort(sortFn);
+		data[i]['timeValue'] = data[i]['stops'][0]['timeValue'];
+	}
+	return data.sort(sortFn);
+}
+
+function sortFn(a,b) {
+	return a['timeValue'] - b['timeValue'];
+}
+
+function calcDateValue(year, month, day, hour, minute) {
+	year = parseInt(year) * 12 * 31 * 24 * 60;
+	month = parseInt(month) * 31 * 24 * 60;
+	day = parseInt(day) * 24 * 60;
+	hour = parseInt(hour) * 60;
+	minute = parseInt(minute);
+	return year+month+day+hour+minute;
+}
+
+new DepartureWorker;