diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8beb308f56e2d2f4e928d0a497201c6afeba1b8e
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,62 @@
+# This file is a template, and might need editing before it works on your project.
+# use the official gcc image, based on debian
+# can use verions as well, like gcc:5.2
+# see https://hub.docker.com/_/gcc/
+image: gcc
+
+# cache outputs to reduce the build time
+cache:
+    paths:
+        - "**/*.o"
+
+build-sbbs:
+  stage: build
+  # instead of calling g++ directly you can also use some build toolkit like make
+  # install the necessary build tools when needed
+  # before_script:
+  #   - apt update && apt -y install make autoconf
+  script:
+    - cd src/sbbs3
+    - make all
+    - make gtkutils
+    - make RELEASE=1 all
+    - make RELEASE=1 gtkutils
+  artifacts:
+    name: sbbs
+    paths:
+      - "src/sbbs3/*.release/*"
+      - "src/sbbs3/*/*.release/*"
+
+build-sexpots:
+  stage: build
+  # instead of calling g++ directly you can also use some build toolkit like make
+  # install the necessary build tools when needed
+  # before_script:
+  #   - apt update && apt -y install make autoconf
+  script:
+    - cd src/sexpots
+    - make RELEASE=1
+  artifacts:
+    name: sexpots
+    paths:
+      - "src/sexpots/*.release/*"
+
+build-syncterm:
+  stage: build
+  # instead of calling g++ directly you can also use some build toolkit like make
+  # install the necessary build tools when needed
+  # before_script:
+  #   - apt update && apt -y install make autoconf
+  script:
+    - cd src/syncterm
+    - make RELEASE=1
+  artifacts:
+    name: syncterm
+    paths:
+      - "src/syncterm/*.release/*"
+
+# run tests using the binary built before
+#test:
+#  stage: test
+#  script:
+#    - ./runmytests.sh
diff --git a/exec/binkit.js b/exec/binkit.js
index b85d7c200fd2843dcd53c6a3586f442f89e1ed05..81953756e52d5e45c4888a58eab6b75cd7259928 100644
--- a/exec/binkit.js
+++ b/exec/binkit.js
@@ -514,7 +514,7 @@ function callout_want_callback(fobj, fsize, fdate, offset, bp)
 	if (this.received_files.indexOf(fobj.name) != -1)
 		return this.file.REJECT;
 	// Reject or skip existing files.
-	if (file_exists(fobj.name)) {
+	if (file_size(fobj.name) > 0) {
 		log(LOG_WARNING, "Inbound file already exists: " + fobj.name);
 		// If the size and date are the same, reject it.
 		if (fsize == file_size(fobj.name) && fdate == file_date(fobj.name))
diff --git a/exec/load/birthdays.js b/exec/load/birthdays.js
index f5e09db903b7c17f06f3299de058c91c670a778b..6782a15d9a87244723c9a512a5c2d9148ed6823b 100644
--- a/exec/load/birthdays.js
+++ b/exec/load/birthdays.js
@@ -18,7 +18,7 @@ load("birthdate.js");
 // Note: month is 0-based, day (of month) is optional and 1-based
 function birthdays(month, day)
 {
-	var u = new User(1);
+	var u = new User;
 	var lastuser = system.stats.total_users;
 	var list = [];
 	for(u.number = 1; u.number <= lastuser; u.number++) {
diff --git a/exec/load/http.js b/exec/load/http.js
index 7ac9e30f137344c5da74bd694ecdaf2fce015c3a..0af0a1970649dd6ff41b16157bd73e85cff812c9 100644
--- a/exec/load/http.js
+++ b/exec/load/http.js
@@ -61,7 +61,7 @@ HTTPRequest.prototype.SetupGet=function(url, referer, base) {
 	this.base=base;
 	this.url=new URL(url, this.base);
 	if(this.url.scheme!='http' && this.url.scheme!='https')
-		throw("Unknown scheme! '"+this.url.scheme+"'");
+		throw new Error("Unknown scheme! '"+this.url.scheme+"' in url:" + url);
 	if(this.url.path=='')
 		this.url.path='/';
 	this.request="GET "+this.url.request_path+" HTTP/1.0";
@@ -77,7 +77,7 @@ HTTPRequest.prototype.SetupPost=function(url, referer, base, data, content_type)
 	this.base=base;
 	this.url=new URL(url, this.base);
 	if(this.url.scheme!='http' && this.url.scheme!='https')
-		throw("Unknown scheme! '"+this.url.scheme+"'");
+		throw new Error("Unknown scheme! '"+this.url.scheme+"' in url: " + url);
 	if(this.url.path=='')
 		this.url.path='/';
 	this.request="POST "+this.url.request_path+" HTTP/1.0";
diff --git a/exec/load/rss-atom.js b/exec/load/rss-atom.js
index 81643ccf386f9fca14471575435b9683689b1c3f..8f018043f15d345e284d599e6ed12b5a10bb155c 100644
--- a/exec/load/rss-atom.js
+++ b/exec/load/rss-atom.js
@@ -109,111 +109,53 @@
 			Array of { type : String, url : String, length : Number } objects
 			for any <enclosure> elements in the item.
 
-		Item.extra (Object)
-
-			If the item/entry contains additional elements not provided for
-			above, they are tacked on to this object in case you may need
-			to access them.  Presumably these will all be E4X XML objects.
-
 */
 
 load("http.js");
 
-// This is really not the way to do things, but meh.
-var toLocal = function(xmlObject) {
-	for each(var element in xmlObject) {
-		element.setName(element.localName());
-		toLocal(element);
-	}
-}
-
-var Feed = function (url, follow_redirects) {
-
-	var Item = function Item(xmlObject) {
-
-		this.id = "";
-		this.title = "";
-		this.date = "";
-		this.author = "";
-		this.body = "";
-		this.content = "";
-		this.link = "";
-		this.enclosures = [];
-		this.extra = {};
-
-		for each(var element in xmlObject) {
-			switch (element.name()) {
-				case 'guid':
-				case 'id':
-					this.id = element;
-					break;
-				case 'pubDate':
-				case 'updated':
-					this.date = element;
-					break;
-				case 'author':
-					this.author = element.text();
-					break;
-				case 'description':
-				case 'summary':
-					this.body = element;
-					break;
-				case 'link':
-					this.link = skipsp(truncsp(element.text()));
-					break;
-				case 'encoded':
-					this.content = element;
-					break;
-				case 'enclosure':
-					this.enclosures.push({
-						type: element.attribute('type'),
-						url: element.attribute("url"),
-						length: parseInt(element.attribute('length'), 10),
-					});
-					break;
-				default:
-					this.extra[element.name()] = element;
-			}
-		}
-
+const Item = function (i) {
+
+	this.id = i.guid.length() ? i.guid[0].toString() : (i.id.length() ? i.id[0].toString() : '');
+	this.title = ''; // uh ...
+	this.date = i.pubDate.length() ? i.pubDate[0].toString() : (i.updated.length() ? i.updated[0].toString() : '');
+	this.author = i.author.length() ? i.author.toString() : '';
+	this.body = i.description.length() ? i.description[0].toString() : (i.summary.length() ? i.summary[0].toString() : '');
+	this.content = i.encoded.length() ? i.encoded.toString() : '';
+	this.link = i.link.length() ? skipsp(truncsp(i.link[0].toString())) : '';
+	this.enclosures = [];
+
+	var enclosures = i.enclosure.length();
+	for (var n = 0; n < enclosures; n++) {
+		this.enclosures.push({
+			type: i.enclosure[n].attribute('type'),
+			url: i.enclosure[n].attribute('url'),
+			length: parseInt(i.enclosure[n].attribute('length'), 10),
+		});
 	}
 
-	var Channel = function (xmlObject) {
-
-		this.title = "a";
-		this.description = "";
-		this.link = "";
-		this.updated = "";
-		this.items = [];
+}
 
-		if (typeof xmlObject.title != "undefined") this.title = xmlObject.title;
+const Channel = function (c) {
 
-		if (typeof xmlObject.description != "undefined") {
-			this.description = xmlObject.description;
-		} else if (typeof xmlObject.subtitle != "undefined") {
-			this.description = xmlObject.subtitle;
-		}
+	this.title = c.title.length() ? c.title[0].toString() : '';
+	this.description = c.description.length() ? c.description[0].toString() : (c.subtitle.length() ? c.subtitle[0].toString() : '');
+	this.link = c.link.length() ? skipsp(truncsp(c.link[0].toString())) : '';
+	this.updated = c.lastBuildDate.length() ? c.lastBuildDate[0].toString() : (c.updated.length() ? c.updated[0].toString() : '');
+	this.items = [];
 
-		// To do: deal with multiple links
-		if (typeof xmlObject.link != "undefined") this.link = skipsp(truncsp(xmlObject.link.text()));
-
-		if (typeof xmlObject.lastBuildDate != "undefined") {
-			this.updated = xmlObject.lastBuildDate;
-		} else if (typeof xmlObject.updated != "undefined") {
-			this.updated = xmlObject.updated;
-		}
+	var items = c.item.length();
+	for (var n = 0; n < items; n++) {
+		this.items.push(new Item(c.item[n]));
+	}
 
-		var items = xmlObject.elements("item");
-		for each(var item in items) {
-			this.items.push(new Item(item));
-		}
+	var entries = c.entry.length();
+	for (var n = 0; n < entries; n++) {
+		this.items.push(new Item(c.entry[n]));
+	}
 
-		var entries = xmlObject.elements("entry");
-		for each(var entry in entries) {
-			this.items.push(new Item(entry));
-		}
+}
 
-	}
+const Feed = function (url, follow_redirects) {
 
 	this.channels = [];
 
@@ -221,14 +163,17 @@ var Feed = function (url, follow_redirects) {
 		var httpRequest = new HTTPRequest();
 		httpRequest.follow_redirects = follow_redirects || 0;
 		var response = httpRequest.Get(url);
-		if (typeof response == "undefined" || response == "") throw "Empty response from server.";
+		if (typeof response == "undefined" || response == "") {
+			throw new Error('Empty response from server.');
+		}
 		var feed = new XML(response.replace(/^<\?xml.*\?>/g, ""));
-		toLocal(feed); // This is shitty
+		httpRequest = undefined;
+		response = undefined;
 		switch (feed.localName()) {
 			case "rss":
-				var channels = feed.elements("channel");
-				for each(var element in channels) {
-					this.channels.push(new Channel(element));
+				var channels = feed.channel.length();
+				for (var n = 0; n < channels; n++) {
+					this.channels.push(new Channel(feed.channel[n]));
 				}
 				break;
 			case "feed":
@@ -237,6 +182,7 @@ var Feed = function (url, follow_redirects) {
 			default:
 				break;
 		}
+		feed = undefined;
 	}
 
 	this.load();
diff --git a/exec/load/sbbslist_lib.js b/exec/load/sbbslist_lib.js
index 585ba0b39813a4713af47a68a9128a2562068af8..2be83a4130298e9e2ffada8a5291c53a3c2723ee 100644
--- a/exec/load/sbbslist_lib.js
+++ b/exec/load/sbbslist_lib.js
@@ -749,7 +749,14 @@ function syncterm_list(list, dir)
             f.writeln(format("\tConnectionType=%s", list[i].service[j].protocol));
             f.writeln(format("\tAddress=%s", list[i].service[j].address));
 			if(list[i].service[j].port)
-				f.writeln(format("\tPort=%s", list[i].service[j].port));	
+				f.writeln(format("\tPort=%s", list[i].service[j].port));
+			if(list[i].service[j].description
+				&& (list[i].service[j].description.toUpperCase().indexOf("PETSCII") == 0
+				|| list[i].service[j].description.toUpperCase().indexOf("COMMODORE") == 0)) {
+				f.writeln(format("\tScreenMode=%s"
+					,list[i].service[j].description.indexOf("80") >= 0 ? "C128-80col" : "C64"));
+				f.writeln("\tNoStatus=true");
+			}
             f.writeln();
         }
     }
diff --git a/exec/load/text.js b/exec/load/text.js
index edeb21f293ad0bb0c3418e5625dc1c4948b5507d..c2b1bbacb03994787e2fb65a6413931c59b8166a 100644
--- a/exec/load/text.js
+++ b/exec/load/text.js
@@ -1,5 +1,3 @@
-/* $Id: text.js,v 1.33 2020/08/01 22:06:51 rswindell Exp $ */
-
 /* Synchronet static text string constants */
 
 /* Automatically generated by textgen $ */
@@ -839,7 +837,9 @@ var QWKSettingsUtf8=827;
 var MsgPostedToYouVia=828;
 var Unlimited=829;
 var NodeConnectionRaw=830;
+var MouseTerminalQ=831;
+var TerminalMouse=832;
 
-var TOTAL_TEXT=831;
+var TOTAL_TEXT=833;
 
 this;