diff --git a/samples/electron_gui/index.html b/samples/electron_gui/index.html
new file mode 100644
index 0000000000..bf8b530327
--- /dev/null
+++ b/samples/electron_gui/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ BOINC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Host:
+
+
Projects:
+
+
+
Tasks:
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/electron_gui/styles.css b/samples/electron_gui/styles.css
new file mode 100644
index 0000000000..932b9ebaed
--- /dev/null
+++ b/samples/electron_gui/styles.css
@@ -0,0 +1,12 @@
+html, body, .container-fluid {
+ height: 100%;
+ #background-color: #111;
+}
+
+html {
+ -webkit-app-region: drag;
+}
+
+.container-fluid {
+ padding: 25px;
+}
diff --git a/samples/electron_gui/window.js b/samples/electron_gui/window.js
new file mode 100644
index 0000000000..3195c0ba8c
--- /dev/null
+++ b/samples/electron_gui/window.js
@@ -0,0 +1,131 @@
+// This is a proof of concept for an electron-based BOINC GUI,
+// showing how to do GUI RPCs to the client.
+//
+// I developed this by starting with one of electron's "simple samples" (activity-monitor)
+// and replacing the files index.html, styles.css, and window.js
+//
+// Run this on a host with a running BOINC client,
+// in a directory with the GUI password file (gui_rpc_auth.cfg).
+//
+// Some next steps:
+// - do periodic RPCs to update state information
+// - add menus and dialogs
+// - Turn the Suspend button into a never/prefs/always control
+
+const os = require('os')
+const fs = require('fs')
+const crypto = require('crypto')
+
+var auth_id = 0;
+var auth_seqno = 1;
+var passwd;
+var http = new XMLHttpRequest();
+
+var state; // result of get_state()
+
+// read RPC password from file
+//
+function read_password() {
+ let x = String(fs.readFileSync("gui_rpc_auth.cfg"));
+ passwd = x.trim();
+}
+
+// initiate a GUI RPC, passing authentication info if needed
+// returns a Promise; passes reply XML to the resolve function
+//
+function gui_rpc(request) {
+ return new Promise(function(resolve, reject) {
+ http.onreadystatechange = function() {
+ if (http.readyState != 4) return;
+ if (http.status == 200) {
+ //console.log('got response: ' + http.responseText);
+ resolve(http.responseText);
+ } else {
+ reject(http);
+ }
+ };
+ http.open("POST", "http://localhost:31416", true);
+ if (auth_id) {
+ http.setRequestHeader("Auth-ID", auth_id);
+ console.log("request "+request+ " auth ID " + auth_id + " seqno " + auth_seqno);
+ http.setRequestHeader("Auth-Seqno", auth_seqno);
+ var s = String(auth_seqno);
+ var x = crypto.createHash('md5').update(s+passwd).digest("hex");
+ http.setRequestHeader("Auth-Hash", x);
+ auth_seqno++;
+ }
+ http.send(request);
+ });
+}
+
+// Do a set_run_mode() RPC
+//
+function set_run_mode(mode) {
+ return gui_rpc("<"+mode+"/>").then(function(reply) {
+ console.log("set run mode");
+ });
+}
+
+// get authentication ID
+//
+function authorize() {
+ read_password();
+ return gui_rpc("").then(function(reply) {
+ x = new DOMParser().parseFromString(reply, "text/xml");
+ auth_id = x.getElementsByTagName("auth_id")[0].childNodes[0].nodeValue;
+ console.log("auth_id: "+auth_id);
+ });
+}
+
+// do a get_state() RPC
+//
+function get_state() {
+ return gui_rpc("").then(function(reply) {
+ //console.log("reply:" + reply);
+ parser = new DOMParser();
+ state = parser.parseFromString(reply, "text/xml");
+ });
+}
+
+function suspend() {
+ set_run_mode("never").then(function() {
+ });
+}
+
+// display the result of get_state()
+//
+function show_state() {
+ // show host name
+ //
+ d = state.querySelector("domain_name").textContent;
+ console.log('domain name', d);
+ document.querySelector('#domain_name').innerHTML = d;
+
+ // show projects
+ //
+ x = '';
+ state.querySelectorAll("project").forEach((p)=>{
+ x += '
'+ p.querySelector('project_name').textContent;
+ });
+ document.querySelector('#projects').innerHTML = x;
+
+ // show tasks
+ x = '';
+ state.querySelectorAll("result").forEach((r)=>{
+ x += '
'+ r.querySelector('name').textContent;
+ });
+ document.querySelector('#tasks').innerHTML = x;
+}
+
+
+$(() => { // shorthand for document ready
+ // get authorization ID
+ //
+ authorize().then(function() {
+ // then get state and show it
+ //
+ get_state().then(function() {
+ show_state();
+ });
+ });
+})