80 Commits

Author SHA1 Message Date
1c7842c182 readme 2017-02-11 13:13:09 +13:00
9e33e50986 display ip addresses 2017-02-09 19:28:43 +13:00
7f618db70a fix not displaying nonzero share sizes 2017-02-08 19:02:20 +13:00
7894355647 popups: only show when manually turning on the feature, not when it's auto-enabled on load 2017-02-08 18:34:12 +13:00
af324441b7 client: clicking the popup refocus browser window, switch to PM tab as necessary 2017-02-08 18:33:17 +13:00
d862a3f703 more consistent comment style for URL references 2017-02-08 18:20:35 +13:00
59c118dc34 remove queryselectorall, always use the fallback implementation 2017-02-08 18:19:57 +13:00
42a8298362 standardise event cancellation 2017-02-08 18:19:33 +13:00
c4e37bf47d explicit falsey checks for some persistent vars (no practical difference) 2017-02-06 16:49:37 +13:00
95d56dbca2 Added tag release-1.1.2 for changeset 7278eb0d067d 2017-02-06 16:44:27 +13:00
761d0bfad5 build: set build version in binary at build time 2017-02-06 16:43:17 +13:00
1d3f16e6c6 change default hub name in sample file 2017-02-06 16:43:07 +13:00
e5ceadb03a doc: changelog 2017-02-06 16:39:27 +13:00
6999069fd7 server: set SERVER header on all requests (even SIO ones), display version on startup 2017-02-06 16:35:25 +13:00
5da61d5922 whitespace in sample config file 2017-02-06 16:27:33 +13:00
d33ba5c085 remove 'extern' from config files 2017-02-06 16:27:14 +13:00
a28e5ce9b0 remove extra /conf/ request 2017-02-06 16:25:49 +13:00
3448cb7eeb don't require extern 2017-02-06 16:20:31 +13:00
ba941adfdd client: fix padding around user count in title 2017-02-06 13:54:24 +13:00
48d96f9efe Added tag release-1.1.1 for changeset d14041daa7bb 2017-02-06 13:46:20 +13:00
1a1f361e60 doc: changelog 2017-02-06 13:45:22 +13:00
3f1f575652 fix minifier generating malformed content 2017-02-06 13:44:56 +13:00
e2af839101 Added tag release-1.1.0 for changeset a2c92b262f33 2017-02-06 12:25:57 +13:00
659576c7e1 doc: changelog 2017-02-06 12:24:50 +13:00
0c5279e254 build: include clientpack.php in source archive 2017-02-06 12:19:05 +13:00
99cf8ebe49 custom favicon support 2017-02-06 12:18:21 +13:00
1189d6fe60 remove unused config.app.name/version 2017-02-06 12:18:07 +13:00
96c3e6ea7d remove unused key from sample config 2017-02-06 12:00:19 +13:00
1a64f79394 client: minify/combine CSS/JS/HTML via extra build step 2017-02-06 11:58:38 +13:00
692537c6f0 client/js: clearer state machine for html5 notification permissions 2017-02-06 11:58:21 +13:00
d2b1d23ba6 client/js: remove dead code (1) 2017-02-06 11:58:00 +13:00
bc80c94a6f client/socket.io: reduce size of MIT boilerplate 2017-02-06 11:57:40 +13:00
fd79d94a20 doc: preliminary changelog (2) 2017-02-05 23:11:52 +13:00
d17ad14914 doc: preliminary changelog 2017-02-05 23:10:55 +13:00
763f5030d1 client: make warn-on-close optional, default false 2017-02-05 22:47:25 +13:00
aa8d89d14b client: display unread message count in the title 2017-02-05 22:43:21 +13:00
d937491545 client: add warning popup if closing the tab while the connection is still open 2017-02-05 20:57:38 +13:00
226e6751c6 client: separate NEW vs NEW-PM titles, move user count to the end of the title 2017-02-05 20:52:24 +13:00
9060b87e94 client: 'unread' support for the main tab 2017-02-05 20:46:50 +13:00
78b7af7fbc css: add some whitespace between bottom of text and the input box 2017-02-05 20:11:10 +13:00
ff075e2e7e client: fix clearing ALL unread flags when clearing a single one 2017-02-05 20:07:28 +13:00
ef9057383d client: fixes for previous 2017-02-05 20:07:15 +13:00
0951bf821e client: page visibility detection infrastructure (but it doesn't do anything yet) 2017-02-05 19:59:43 +13:00
a60e4bb0c2 client: fix closed tabs coming back from the dead in some cases 2017-02-05 19:43:31 +13:00
7b5dbde7a6 client: add disconnected/reconnected messages into PM windows 2017-02-05 19:43:23 +13:00
3c2f0c7368 html5 notifications (disabled by default) for PMs when PM tab is not active 2017-02-05 19:35:59 +13:00
1cc6cd2b90 patch issue in r46 2017-02-05 19:09:08 +13:00
3f077f15f0 client: support shift+tab to autocomplete backward 2017-02-05 18:29:44 +13:00
ade4439f92 server: add log message on successful upstream connection 2017-02-05 18:24:32 +13:00
edffaac74b server: remove dead code 2017-02-05 18:24:22 +13:00
ceb7420e18 client: display join/parts for PM tabs (regardless of global join/part setting) 2017-02-05 18:13:45 +13:00
56ced01e97 client: clickable magnet links 2017-02-05 18:09:53 +13:00
3c1db1266e client: add noreferrer to outgoing links 2017-02-05 18:06:18 +13:00
4a4e9e694d client: custom date formats (minutes/seconds/full) 2017-02-05 18:03:44 +13:00
c619be5917 client: chat scrollback: clamp to edges rather than wrapping 2017-02-05 17:49:59 +13:00
afd190b7cc client: chat scrollback with ctrl+up / ctrl+down 2017-02-05 17:48:25 +13:00
a927add8c5 client: clearer state transitions, separate disconnected state 2017-02-05 17:35:45 +13:00
0a5db6a015 client: patch one case of not clearing userlist, set spellcheck attr when in text-entry mode 2017-02-05 17:28:35 +13:00
e15a6afceb client: automatically reconnect with same username if we lose connection to the server 2017-02-05 17:17:51 +13:00
c19b0fe521 also load stock username text when we get a reconnect page 2017-02-05 17:15:20 +13:00
1eaabb099d client: only clear the screen on first load, not on refresh 2017-02-05 17:09:18 +13:00
b26b3a695e prevent displaying sentinel pass if no pass was used 2017-02-05 17:06:58 +13:00
b30beac2b5 obfuscate saved password when re-logging in 2017-02-05 16:58:13 +13:00
39e84f2744 client: display description/email/client tag/share size on hover, display operators in green 2017-02-05 16:47:54 +13:00
7ceed88dfa server: send user details 2017-02-05 16:47:39 +13:00
c7e40ab6c1 whitespace 2017-02-05 16:14:57 +13:00
35994e81c6 drop dead code 2017-02-05 16:14:02 +13:00
2c94713ba0 persistence_set/get functions, save the join/part status 2017-02-05 16:11:52 +13:00
e9b3fedb17 save last username/pass for login 2017-02-05 16:03:43 +13:00
d0219cb16a fix superfluous nl2br 2017-02-05 15:58:19 +13:00
d0d52e931e favicon: include 256x256 32bpp in the icon 2017-02-05 15:57:09 +13:00
0ec9ae30b4 embed MIT license in socket.io JS file 2017-02-05 15:54:23 +13:00
490d20b807 bump bundled socket.io from 1.4.5 to 1.7.2 2017-02-05 15:48:59 +13:00
299a1c12e1 remove some very old pre-tab private message stuff 2017-02-05 15:43:52 +13:00
a9dc45f727 css: also support consecutive spaces in system messages 2017-02-05 15:43:16 +13:00
5f8544f031 css: fix malformed text-size-adjust parameters 2017-02-05 15:41:28 +13:00
ea87419abc css: support consecutive spaces in chat messages 2017-02-05 15:40:44 +13:00
d5e8e051e3 conf: new Web>ExternalWebroot key to load from /client/ directory 2017-02-05 15:37:27 +13:00
656125e790 conf: remove App>Debug key 2017-02-05 15:34:32 +13:00
0892666ead Added tag release-1.0.2 for changeset 46fe53368241 2016-11-29 20:19:35 +13:00
14 changed files with 726 additions and 194 deletions

View File

@@ -2,6 +2,7 @@ mode:regex
\.exe$
^nmdc-webfrontend\.conf$
^clientpack/
^_dist/
^bindata\.go$

View File

@@ -1,2 +1,6 @@
769fad81e3f8db8f7e5f5c164656a382a169d735 release-1.0.0
9ed95938d809a8226aca529e34b655e6d8c8c379 release-1.0.1
46fe533682419c8a519836ac95b5575053aa0fa8 release-1.0.2
a2c92b262f339f82eb01c8d92dda252a27432255 release-1.1.0
d14041daa7bbbd37ea2ff47aa978b9595af67ca3 release-1.1.1
7278eb0d067d8ed2a653de6a1feeeb7f76fb9891 release-1.1.2

View File

@@ -2,17 +2,16 @@ package main
type Config struct {
App struct {
Name string `json:"name"`
Version string `json:"version"`
MotdHTML string `json:"motd"`
Debug bool `json:"debug"`
}
Web struct {
Port int `json:"port"`
BindTo string `json:"bind_to"`
Extern string `json:"extern"`
Title string `json:"title"`
Port int `json:"port"`
BindTo string `json:"bind_to"`
Title string `json:"title"`
CustomFavicon bool `json:"custom_favicon"`
ExternalWebroot bool `json:"external_webroot,omitempty"`
}
Hub struct {

View File

@@ -8,10 +8,58 @@ Tags: nmdc
=UPGRADING FROM DCWEBUI2=
- The configuration file format is identical, but please now ensure it's valid json instead of just a .js file. This means no assignment, use double-quoted strings, and no comments.
- The configuration file content is identical between nmdc-webfrontend 1.0.0 and dcwebui2 1.3.0, but please now ensure it's valid JSON instead of arbitrary javascript. This means no assignment, use double-quoted strings, and no comments.
- Future changes to the configuration file since nmdc-webfrontend 1.0.0 are backward compatible (see the changelog for more details).
=CHANGELOG=
2017-02-11 1.1.3
- Feature: Display user IP address on hover, if available
- Enhancement: Allow clicking on popup notifications
- Enhancement: Only show 'popups enabled' notification when enabling for the first time, not persistent page load
- Update libnmdc to 0.13
- Fix a cosmetic issue with not displaying non-zero user share sizes
2017-02-06 1.1.2
- Autodetect 'extern', no need to include it in config files
- Enhancement: Remove redundant request, for a faster page load
- Display server version number in log file and in response headers
- Fix a cosmetic issue with spacing around user count in page title
2017-02-06 1.1.1
- Fix an issue with malformed content in minified build
2017-02-06 1.1.0
- Feature: Remember last username/password for login; remember last "show joins/parts" status
- Feature: Display user details on hover (description, email, client tag, share size)
- Feature: Optional desktop notifications for background PMs (not possible in incognito)
- Feature: Automatically reconnect with the same username/password if connection was lost
- Feature: Re-enter last message (Ctrl+Up, Ctrl+Down)
- Feature: Set custom date/time format (Minutes, Seconds, Full), remembered for next session
- Feature: Clickable magnet links
- Feature: Display unread main-chat message count in the page title if the window is inactive
- Feature: Add warning message when closing tab while still connected (optional preference, disabled by default, will be remembered).
- Feature: Admin option to load a custom favicon (set `web.custom_favicon=true` and place a `favicon.ico` in the current directory)
- Feature: Admin option to use external web resources (set `web.external_webroot=true` and use the /client/ directory)
- Enhancement: Higher resolution favicon
- Enhancement: Display operators in green in the user list
- Enhancement: Enable spellcheck for text input once logged in
- Enhancement: Prevent sending referrer to remote URLs
- Enhancement: Display joins/parts and connection/disconnection messages in PM tabs
- Enhancement: Support Shift+Tab to autocomplete backward
- Enhancement: Support unread status for the main tab
- Enhancement: Improve page load time via minification
- Remove unused options from the config file
- Update socket.io to 1.7.2
- Update libnmdc to 0.12
- Add margin between bottom of the text area and the text input box
- Fix a cosmetic issue with collapsing consecutive spaces in posted messages
- Fix a cosmetic issue with text size adjustment on mobile devices
- Fix a cosmetic issue with clearing the screen on reconnection
- Fix a cosmetic issue with not clearing the userlist on certain types of network error
- Fix a cosmetic issue with closed PM tabs reappearing in some cases
- Fix a cosmetic issue with marking all PM tabs as read when switching to a single one
2016-11-29 1.0.2
- Rebuild with libnmdc 0.11
- Fix an issue with not setting a version in the client tag

View File

@@ -88,7 +88,7 @@ single_build() {
# GOARCH/GOOS supplied in function env
go build \
-a \
-ldflags '-s -w' \
-ldflags "-s -w -X main.VERSION=nmdc-webfrontend/${version}" \
-gcflags "-trimpath=${GOPATH}" \
-asmflags "-trimpath=${GOPATH}" \
-o "$(pathfix "${tmpdir}/${local_bin_name}")"
@@ -158,8 +158,9 @@ main() {
if [[ -f ./bindata.go ]] ; then
rm ./bindata.go
fi
go-bindata -nomemcopy -prefix client client
php clientpack.php
go-bindata -nomemcopy -prefix clientpack clientpack
GOARCH=amd64 GOOS=windows single_build "$version"
GOARCH=386 GOOS=windows single_build "$version"
GOARCH=amd64 GOOS=linux single_build "$version"
@@ -170,6 +171,7 @@ main() {
local SOURCE_FILES=(
client/
build.sh
clientpack.php
Config.go
main.go
nmdc-webfrontend.conf.SAMPLE

View File

@@ -1,9 +1,9 @@
/* dcwebui.css */
* {
-webkit-text-size-adjust:100%
-moz-text-size-adjust:100%
-ms-text-size-adjust:100%
-webkit-text-size-adjust:100%;
-moz-text-size-adjust:100%;
-ms-text-size-adjust:100%;
}
html,body {
@@ -180,13 +180,13 @@ html,body {
overflow-y:auto;
-webkit-transition: all 0.25s ease-in-out;
padding:0; /*4px;*/
padding:0;
margin:0;
box-sizing:border-box;
position:absolute;
top:4px;
bottom:0;
bottom:4px;
left:4px;
right:0;
}
@@ -300,6 +300,9 @@ html,body {
/* Text */
.tx-sys, .tx-user, .tx-chat {
white-space:pre-wrap;
}
.tx-time {
color:grey;
}
@@ -314,10 +317,6 @@ html,body {
.tx-chat {
color: black;
}
.tx-private {
color: red;
font-style: italic;
}
/* webkit scrollbars */
@@ -354,6 +353,11 @@ html,body {
overflow-y: auto;
}
.user-is-operator {
color:darkgreen;
font-weight:bold;
}
/* User sprite */
.ul-mini {

View File

@@ -1,30 +1,35 @@
/* dcwebui.js */
//;(function() {
//IIFEMODE:;(function() {
"use strict";
var $ = (document.querySelectorAll ?
function(s) {
var r = document.querySelectorAll(s);
return (s[0] === '#' && r.length === 1) ? r[0] : r;
} :
function(s) {
// i'm not writing a selector engine...
if (! s.length) return [];
if (s[0] === '#') {
return document.getElementById(s.slice(1));
} else if (s[0] === '.') {
return document.getElementsByClassName(s.slice(1));
} else {
return document.getElementsByTagName(s);
}
var SENTINEL_PASSWORD = "************";
var CHAT_SCROLLBACK_LIMIT = 50; // Once over 2x $limit, the first $limit will be trimmed off the list
var EXTERN_ROOT = window.location.protocol + "//" + window.location.host + "/";
var $ = function(s) {
// There used to be a querySelectorAll implementation, but, better that we don't have
// potentially-incompatible implementations if this one does actually work.
// i'm not writing a selector engine...
if (! s.length) {
return [];
}
);
if (s[0] === '#') {
return document.getElementById(s.slice(1)); // single element
} else if (s[0] === '.') {
return document.getElementsByClassName(s.slice(1)); // multiple elements
} else {
return document.getElementsByTagName(s); // multiple elements
}
};
var nmdc_escape = function(str) {
return (''+str).length ? (''+str).
replace(/&/g,'&').replace(/\|/g,'|').replace(/\$/g,'$') :
' ';
return (
(''+str).length
? (''+str).replace(/&/g,'&').replace(/\|/g,'|').replace(/\$/g,'$')
: ' '
);
};
var hesc = function(s) {
@@ -34,22 +39,38 @@ var hesc = function(s) {
return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; });
};
var nl2br = function(str) { // thanks php.js!
return (str+'').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2');
var fmtBytes = function(b) {
if (b == 0) {
return '(nothing)';
}
var k = 1024;
var sizes = [' B', ' KiB', ' MiB', ' GiB', ' TiB'];
var i = Math.floor(Math.log(b) / Math.log(k));
return parseFloat((b / Math.pow(k, i)).toFixed(3)) + sizes[i];
};
var urldesc = function(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
}
var linkify = function(str) {
return str.replace(
/(https?:\/\/[^\s<]+)/g, "<a target='_blank' href=\"$1\">$1</a>"
// n.b. str is already hesced
return (str
.replace(
/(https?:\/\/[^\s<]+)/g,
"<a target='_blank' rel=\"noreferrer\" href=\"$1\">$1</a>"
)
.replace(
/magnet:\?.+dn=([^\< ]+)/g,
function(match, m1) { return "<a href=\"" + match + "\">[MAGNET] " + urldesc(m1) + "</a>"; }
)
);
};
var sanitise = function(s) {
return linkify(nl2br(hesc(s)));
};
var toggle = function($el) {
$el.style.display = ($el.style.display === "block") ? "none" : "block";
return linkify(hesc(s));
};
var textContent = function($el) {
@@ -58,7 +79,23 @@ var textContent = function($el) {
return "";
};
// https://gist.github.com/eligrey/1276030
var negmod = function(l, r) {
var ret = l % r;
if (l < 0) {
return ret + r;
} else {
return ret;
}
};
// @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
var b64 = function(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
})).replace(/=/g, '');
}
// @ref https://gist.github.com/eligrey/1276030
var appendInnerHTML = function($el, html) {
var child = document.createElement("span");
child.innerHTML = html;
@@ -69,7 +106,7 @@ var appendInnerHTML = function($el, html) {
}
};
// http://stackoverflow.com/a/5598797
// @ref http://stackoverflow.com/a/5598797
function getOffsetLeft( elem ) {
var offsetLeft = 0;
do {
@@ -90,6 +127,55 @@ function getOffsetTop( elem ) {
return offsetTop;
}
/* */
var date_format = function(d, format) {
var pad = function(s) {
return (s < 10) ? '0'+s : ''+s ;
};
var ret = format;
ret = ret.replace(/H/g, pad(d.getHours()));
ret = ret.replace(/i/g, pad(d.getMinutes()));
ret = ret.replace(/s/g, pad(d.getSeconds()));
ret = ret.replace(/Y/g, d.getFullYear());
ret = ret.replace(/m/g, pad(d.getMonth() + 1));
ret = ret.replace(/d/g, pad(d.getDate()));
return ret;
};
/* */
var notify = function(title, body, tab) {
if (!("Notification" in window)) {
return; // not supported by browser
}
switch (window.Notification.permission) {
case "granted": {
var n = new Notification(title, {
body: body,
icon: EXTERN_ROOT + "/favicon.ico"
});
n.onclick = function() {
parent.focus(); // recent chrome
window.focus(); // older browsers
tab_set(tab);
this.close();
};
} break;
case "denied": return;
default: {
// Clarify permission and retry
Notification.requestPermission(function(permission) {
notify(title, body);
});
} break;
}
};
/* Tab writers */
var write = function(tab) {
@@ -111,15 +197,7 @@ var write = function(tab) {
return this.raw('<span class="'+c+'">'+s+'</span>');
},
'time': function() {
var d = new Date();
var pad = function(s) {
return (s < 10) ? '0'+s : ''+s ;
};
return this.raw(
'<span class="tx-time">['+
pad(d.getHours()) + ":" + pad(d.getMinutes())+
"]</span> "
);
return this.raw('<span class="tx-time">['+ date_format(new Date(), timestamp_formats[timestamp_format_index])+"]</span> ");
},
'system': function(s) {
return this.time().c('tx-sys', sanitise(s)).raw('<br/>');
@@ -131,17 +209,16 @@ var write = function(tab) {
return this.time().
pubnick(u).raw('&nbsp;').
c('tx-chat', sanitise(s)).raw('<br/>');
},
'priv': function(f, t, s) {
return this.time().
c('tx-user', '&lt;'+hesc(f)+'&nbsp;-&gt;&nbsp'+hesc(t)+'&gt;').
raw('&nbsp').
c('tx-private', sanitise(s)).
raw('<br/>');
}
};
};
var write_system_message_in_all_pm_tabs = function(system_message) {
for (var k in pm_tabs) {
writerFor(k).system(system_message);
}
};
/* Userlist */
var switchToPM = function(u) {
@@ -171,8 +248,7 @@ var userMenu = function(u, ev) {
usermenu.show();
//
ev.preventDefault();
return false;
return noprop(ev);
};
var userlist = {
@@ -184,6 +260,7 @@ var userlist = {
var userlist = userlists[l];
var to_add = document.createElement('li');
to_add.className = "user-" + b64(u);
to_add.innerHTML = hesc(u);
to_add.onclick = function() { switchToPM(u); };
@@ -245,16 +322,38 @@ var userlist = {
return ret;
},
'has': function(u) {
var userlist = $(".userlist")[0].children;
for (var i = 0, e = userlist.length; i < e; ++i) {
if (textContent(userlist[i]) === u) {
return true;
}
}
return false;
return $(".user-" + b64(u)).length !== 0; /* there are two - large and non-large */
},
'count': function() {
return $(".userlist")[0].children.length;
},
'setInfo': function(nick, props) {
var baseClass = "user-" + b64(nick);
var $el = $("." + baseClass);
var prop_str = [];
if (props.Description.length > 0) {
prop_str.push(props.Description);
}
if (props.Email.length > 0) {
prop_str.push(props.Email);
}
if (props.ClientTag.length > 0) {
prop_str.push(props.ClientTag + " " + props.ClientVersion);
}
if (props.IPAddress.length > 0) {
prop_str.push(props.IPAddress);
}
prop_str.push("Sharing " + fmtBytes(props.ShareSize));
for (var i = 0; i < $el.length; ++i) {
$el[i].title = prop_str.join("\n");
if (props.IsOperator) {
$el[i].className = baseClass + " user-is-operator";
} else {
$el[i].className = baseClass; // remove op flag
}
}
}
};
@@ -262,17 +361,29 @@ var submit = function() {
var str = $("#chatbox").value;
if (! str.length) return;
if (hub_state === 0) {
hub_state = 1;
if (hub_state === STATE_READY_FOR_LOGIN) {
transition(1); // disables #chatbox
var name_parts = str.split(":", 2);
hub_last_nick = name_parts[0];
sock.emit('hello', {'nick' : hub_last_nick, 'pass' : name_parts.length >= 2 ? name_parts[1] : ''});
$("#chatbox").disabled = true;
hub_last_nick = str.split(":", 2)[0];
var hub_pass = "";
if (str.length > hub_last_nick.length) {
hub_pass = str.substr(hub_last_nick.length + 1);
}
if (hub_pass === SENTINEL_PASSWORD) {
// Probably not a real password. Attempt to load a better one from the saved state
var cache = persistence_get("login", "");
if (cache.indexOf(":") != -1) {
hub_pass = cache.substr(cache.indexOf(":") + 1);
}
}
persistence_set("login", hub_pass.length > 0 ? hub_last_nick+":"+hub_pass : hub_last_nick);
sock.emit('hello', {'nick' : hub_last_nick, 'pass' : hub_pass});
write("tab-main").system("Connecting...");
} else if (hub_state === 2) {
} else if (hub_state === STATE_ACTIVE) {
if (pm_target !== false) {
sock.emit('priv', {'user': pm_target, 'message': str});
writerFor(pm_target).pub(hub_last_nick, str );
@@ -280,6 +391,12 @@ var submit = function() {
sock.emit('pub', {'message' : str});
}
chat_scrollback.push( str );
chat_scrollback_index = -1;
if (chat_scrollback.length > (2*CHAT_SCROLLBACK_LIMIT)) {
chat_scrollback = chat_scrollback.slice(CHAT_SCROLLBACK_LIMIT);
}
} else {
write("tab-main").system("Invalid internal state.");
}
@@ -287,6 +404,41 @@ var submit = function() {
$("#chatbox").value = '';
};
/* page visibility */
var pagevis_currently_visible = true;
var pagevis_setup = function(fnActive, fnInactive) {
var h, vc;
if (typeof document.hidden !== "undefined") {
h = "hidden";
vc = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
h = "msHidden";
vc = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
h = "webkitHidden";
vc = "webkitvisibilitychange";
}
if (typeof document[h] === "undefined") {
// Browser doesn't support Page Visibility API, so behave as if the page is always visible
pagevis_currently_visible = true
} else {
document.addEventListener(vc, function() {
if (document[h]) {
pagevis_currently_visible = false;
fnInactive();
} else {
pagevis_currently_visible = true;
fnActive();
}
});
}
}
/* tabs */
/**
@@ -305,7 +457,13 @@ var tab_set = function(tab) {
var tabitems = $(".tabitem");
for (var i in tabitems) {
try {
tabitems[i].className = "tabitem" + (tabitems[i].getAttribute('data-tab') === tab ? ' selected' : '');
// Update UNREAD/SELECTED flags for the target
var was_unread = (tabitems[i].className.indexOf("unread") !== -1);
var is_target = (tabitems[i].getAttribute('data-tab') === tab);
var is_still_unread = (was_unread && !is_target);
tabitems[i].className = "tabitem" + (is_target ? ' selected' : '') + (is_still_unread ? ' unread' : '');
} catch (e) {};
}
@@ -317,11 +475,16 @@ var tab_set = function(tab) {
}
}
if (tab == "tab-main" && pagevis_currently_visible) {
mainchat_unread_count = 0;
}
updateTitle();
write(tab).scroll();
$("#chatbox").focus();
last_tab = tab;
};
var tab_new = function(id, name) {
@@ -355,7 +518,8 @@ var tab_free = function(id) {
// clear from PM tabs
for (var i in pm_tabs) {
if (pm_tabs[i] === id) {
pm_tabs[i] = false;
// pm_tabs[i] = false;
delete(pm_tabs[i]);
}
}
@@ -368,6 +532,20 @@ var tab_free = function(id) {
}
};
var noprop = function(ev) {
if (ev.preventDefault) {
ev.preventDefault();
}
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true; // oldIE
}
return false;
}
var tab_addHandlers = function() {
var tabitems = $(".tabitem");
for (var i = 0; i < tabitems.length; i++) {
@@ -376,13 +554,7 @@ var tab_addHandlers = function() {
tabitems[i].onclick = function(ev) {
tab_set( this.getAttribute('data-tab') );
// 360nobubble
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true; // oldIE
}
return false;
return noprop(ev);
};
}
@@ -393,28 +565,30 @@ var tab_addHandlers = function() {
tabclosers[i].onclick = function(ev) {
tab_free( this.getAttribute('data-tab') );
// 360nobubble
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true; // oldIE
}
return false;
return noprop(ev);
};
}
};
/* */
var writerFor = function(username) {
var tabid = "";
var maybeWriterFor = function(username) {
if (! username in pm_tabs || ! pm_tabs[username]) {
tabid = tab_new(next_tabid++, username);
pm_tabs[username] = tabid;
} else {
tabid = pm_tabs[username];
return null;
}
return write(pm_tabs[username]);
};
var writerFor = function(username) {
var ret = maybeWriterFor(username);
if (ret !== null) {
return ret; // found
}
// create new
var tabid = tab_new(next_tabid++, username);
pm_tabs[username] = tabid;
return write(tabid);
};
@@ -422,7 +596,7 @@ var writerFor = function(username) {
var tabcomplete_state = '';
var tabcompletion_start = function() {
var tabcompletion_start = function(direction) {
var cursor = $("#chatbox").value.replace(/^.*\s([^\s]+)$/, '$1');
@@ -456,7 +630,7 @@ var tabcompletion_start = function() {
}
}
var targetName = match[(i + 1) % match.length];
var targetName = match[negmod(i + direction, match.length)];
// Replace in textbox
@@ -516,7 +690,10 @@ var menu = new MenuList($("#menubutton"));
menu.reset = function() {
this.clear();
this.add(joinparts_getstr(), toggle_joinparts);
this.add(joinparts_getstr(), toggle_joinparts);
this.add(desktop_notifications_fmtstr(), desktop_notifications_toggle);
this.add(warnonclose_fmtstr(), warnonclose_toggle);
this.add(timestamp_display(), timestamp_toggle);
};
var usermenu = new MenuList(document.body);
@@ -597,22 +774,28 @@ var joinparts_getstr = function() {
var toggle_joinparts = function(ev) {
var $el = ev.target || ev.srcElement;
show_joins = ! show_joins;
persistence_set("show_joins", show_joins);
$el.innerHTML = joinparts_getstr();
};
/* */
var updateTitle = function() {
document.title =
($(".unread").length ? "[NEW PM] " : "") +
"("+userlist.count()+") "+
hub_hubname;
var prefix = "";
var unrTabs = $(".unread");
if (unrTabs.length === 1 && unrTabs[0].getAttribute('data-tab') == "tab-main") {
prefix = "[" + mainchat_unread_count + " NEW] "
} else if (unrTabs.length > 0) {
prefix = "[NEW PM] "
}
document.title = prefix + hub_hubname + " ("+userlist.count()+")"
};
var sock = {};
var hub_state = 0; // [disconnected, sent-nick, connected]
var hub_last_nick = '';
var hub_hubname = 'DCWebUI';
var hub_hubname = "Loading...";
var pm_tabs = {}; // nick => tabid
var next_tabid = 1;
@@ -622,30 +805,174 @@ var last_tab = "tab-main";
var show_joins = false;
var have_cleared_once = false;
var STATE_DISCONNECTED = -1;
var STATE_READY_FOR_LOGIN = 0;
var STATE_CONNECTING = 1;
var STATE_ACTIVE = 2;
var chat_scrollback = [];
var chat_scrollback_index = -1;
var timestamp_formats = [ "H:i", "H:i:s", "Y-m-d H:i:s" ];
var timestamp_names = [ "Minutes", "Seconds", "Full" ];
var timestamp_format_index = 0;
var mainchat_unread_count = 0;
var should_warn_on_close = false;
var timestamp_display = function() {
return "Timestamp format: " + timestamp_names[timestamp_format_index];
};
var timestamp_toggle = function(ev) {
var $el = ev.target || ev.srcElement;
timestamp_format_index = (timestamp_format_index + 1) % timestamp_formats.length;
persistence_set("timestamps", timestamp_format_index);
$el.innerHTML = timestamp_display();
};
var warnonclose_fmtstr = function() {
return (should_warn_on_close ? "&#9745;" : "&#9744;") + " Confirm closing window";
};
var warnonclose_toggle = function(ev) {
var $el = ev.target || ev.srcElement;
should_warn_on_close = !should_warn_on_close;
persistence_set("warnonclose", should_warn_on_close);
$el.innerHTML = warnonclose_fmtstr();
};
var desktop_notifications_enabled = false;
var desktop_notifications_fmtstr = function() {
return (desktop_notifications_enabled ? "&#9745;" : "&#9744;") + " Show desktop popups";
};
var desktop_notifications_toggle = function(ev) {
var $el = ev.target || ev.srcElement;
desktop_notifications_enabled = !desktop_notifications_enabled;
persistence_set("notifications", desktop_notifications_enabled);
if (desktop_notifications_enabled) {
notify(hub_hubname, "Desktop popups enabled", "tab-main");
}
persistence_set("popups", desktop_notifications_enabled);
$el.innerHTML = desktop_notifications_fmtstr();
};
var scrollback_move = function(delta) {
if (chat_scrollback.length === 0) {
return; // no effect
}
if (chat_scrollback_index === -1) {
chat_scrollback_index = chat_scrollback.length - 1; // always starts at most recent message
} else {
chat_scrollback_index += delta;
// clamp
if (chat_scrollback_index === -1) {
chat_scrollback_index = 0;
} else if (chat_scrollback_index === chat_scrollback.length) {
chat_scrollback_index = chat_scrollback.length - 1;
}
}
$("#chatbox").value = chat_scrollback[chat_scrollback_index];
};
/* */
var persistence_set = function(key, value) {
if (window.localStorage) {
window.localStorage[key] = JSON.stringify(value);
}
};
var persistence_get = function(key, fallback) {
try {
return JSON.parse( window.localStorage[key] );
} catch (ex) {
return fallback;
}
};
var transition = function(new_state) {
hub_state = new_state;
switch(new_state) {
case STATE_DISCONNECTED: {
userlist.clear();
$("#chatbox").disabled = true;
$("#chatbox").value = ''; // clear
} break;
case STATE_READY_FOR_LOGIN: {
userlist.clear();
$("#chatbox").spellcheck = false;
$("#chatbox").disabled = false;
$("#chatbox").value = ''; // clear
} break;
case STATE_CONNECTING: {
$("#chatbox").disabled = true;
} break;
case STATE_ACTIVE: {
write("tab-main").system("Now talking on "+hub_hubname);
$("#chatbox").disabled = false;
$("#chatbox").spellcheck = true;
} break;
}
};
var tab_is_visible = function(tabref) {
return (last_tab === tabref && pagevis_currently_visible);
}
var tab_mark_unread = function(tabref) {
if ($("#tabitem-"+tabref).className.indexOf('unread') === -1) {
$("#tabitem-"+tabref).className += " unread";
updateTitle();
}
}
window.onload = function() {
write("tab-main").system("Communicating with server...");
document.title = DCWEBUI_CONF.title;
show_joins = persistence_get("show_joins", false);
document.title = hub_hubname; // "Loading...";
// HTML event handlers
$("#form-none").onsubmit = function(ev) {
submit();
// don't submit form
ev.preventDefault();
return false;
return noprop(ev); // don't submit form
};
$("#chatbox").onkeydown = function(ev) {
if (ev.keyCode === 9) {
// tab
tabcompletion_start();
ev.preventDefault();
return false;
if (ev.keyCode === 9 /* Tab */) {
tabcompletion_start( ev.shiftKey ? -1 : 1 );
return noprop(ev);
} else if (ev.keyCode == 38 /* ArrowUp */ && ev.ctrlKey) {
scrollback_move(-1);
return noprop(ev);
} else if (ev.keyCode == 40 /* ArrowDown */ && ev.ctrlKey) {
scrollback_move(1);
return noprop(ev);
} else {
tabcompletion_inactive();
chat_scrollback_index = -1; // clear
}
};
@@ -660,9 +987,7 @@ window.onload = function() {
$("#menubutton").onclick = function(ev) {
menu.toggle();
ev.preventDefault();
ev.stopPropagation();
return false;
return noprop(ev);
};
window.onclick = function() {
@@ -670,10 +995,38 @@ window.onload = function() {
usermenu.hide();
};
window.onbeforeunload = function (e) {
if (should_warn_on_close && hub_state === STATE_ACTIVE) {
// n.b. recent Firefox / Chrome don't display the custom message here.
var confirmationMessage = "Still connected to the hub \"" + hub_hubname + "\".\nAre you sure you want to close?";
e.returnValue = confirmationMessage;
return confirmationMessage;
}
// else: ignore
};
timestamp_format_index = persistence_get("timestamps", 0);
should_warn_on_close = persistence_get("warnonclose", false);
desktop_notifications_enabled = persistence_get("popups", false);
menu.reset();
tab_addHandlers();
pagevis_setup(
function() {
// We've just become active
// If the foreground is a PM window with UNREAD, mark it as read
tab_set(last_tab);
},
function() {
// We've just become inactive
// TODO: marker lines
}
);
// Hacks for WiiU user-agent
if (navigator && navigator.userAgent && !!navigator.userAgent.match("Nintendo WiiU")) {
@@ -682,14 +1035,29 @@ window.onload = function() {
// Socket event handlers
sock = io.connect(DCWEBUI_CONF.extern);
sock = io.connect(EXTERN_ROOT);
sock.on('cls', function() {
write("tab-main").cls();
userlist.clear();
hub_state = 0;
transition(STATE_READY_FOR_LOGIN);
var pre_login = persistence_get("login", "");
if (pre_login.indexOf(":") !== -1) {
pre_login = pre_login.substr(0, pre_login.indexOf(":")) + ":" + SENTINEL_PASSWORD;
}
$("#chatbox").value = pre_login;
if (have_cleared_once) {
// re-log-in automatically
write("tab-main").system("Automatically reconnecting as \"" + pre_login + "\"...");
submit();
}
if (!have_cleared_once) {
write("tab-main").cls();
userlist.clear();
have_cleared_once = true; // don't re-clear, keep history
}
});
sock.on('hubname', function(s) {
write("tab-main").system("Now talking on "+s);
hub_hubname = s;
updateTitle();
});
@@ -701,25 +1069,40 @@ window.onload = function() {
});
sock.on('pub', function(data) {
write("tab-main").pub( data.user, data.message);
});
sock.on('priv', function(data) {
writerFor(data.user).pub( data.user, data.message);
if (last_tab !== pm_tabs[data.user] &&
($("#tabitem-"+pm_tabs[data.user]).className.indexOf('unread') === -1)
) {
$("#tabitem-"+pm_tabs[data.user]).className += " unread";
if (! tab_is_visible("tab-main")) {
tab_mark_unread("tab-main");
mainchat_unread_count += 1;
updateTitle();
}
});
sock.on('priv', function(data) {
writerFor(data.user).pub(data.user, data.message);
if (! tab_is_visible(pm_tabs[data.user])) {
// Got PM, but tab isn't focused
tab_mark_unread( pm_tabs[data.user] );
if (desktop_notifications_enabled) {
notify("Message from " + data.user, data.message, pm_tabs[data.user]);
}
}
});
sock.on('hello', function() {
hub_state = 2;
$("#chatbox").disabled = false;
transition(STATE_ACTIVE);
write_system_message_in_all_pm_tabs("Reconnected.");
});
sock.on('part', function(u) {
userlist.del(u.user);
if (show_joins) {
write("tab-main").system("*** Parts: "+u.user); // sanitised
}
var targetTab = maybeWriterFor(u.user);
if (targetTab !== null) {
targetTab.system("*** Parts: "+u.user);
}
updateTitle();
});
sock.on('join', function(u) {
@@ -727,21 +1110,33 @@ window.onload = function() {
if (show_joins) {
write("tab-main").system("*** Joins: "+u.user);
}
var targetTab = maybeWriterFor(u.user);
if (targetTab !== null) {
targetTab.system("*** Joins: "+u.user);
}
updateTitle();
});
sock.on('info', function(u) {
try {
var props = JSON.parse(u.message);
userlist.setInfo(u.user, props);
} catch (ex) {}
});
sock.on('close', function() {
hub_state = 0;
userlist.clear();
transition(STATE_DISCONNECTED);
write("tab-main").system("Connection closed by remote host.");
write_system_message_in_all_pm_tabs("Disconnected.");
});
sock.on('disconnect', function() {
hub_state = 0;
userlist.clear();
transition(STATE_DISCONNECTED);
write("tab-main").system("Lost connection to the server.");
write_system_message_in_all_pm_tabs("Disconnected.");
});
sock.on('usercommand', function(data) {
process_usercommand(data);
});
};
//})();
//IIFEMODE:})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -12,7 +12,7 @@
<body>
<div class="tabbar placement-top" id="bar">
<div class="menubutton" id="menubutton">&#9776;</div>
<div class="tabitem selected" data-tab="tab-main">
<div class="tabitem selected" data-tab="tab-main" id="tabitem-tab-main">
<span class="tab-label">
Main
</span>
@@ -52,8 +52,7 @@
</form>
</div>
<script type="text/javascript" src="/socket.io-1.4.5.js"></script>
<script type="text/javascript" src="/conf"></script>
<script type="text/javascript" src="/socket.io-1.7.2.js"></script>
<script type="text/javascript" src="/dcwebui.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

56
clientpack.php Normal file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/php
<?php
# Dependencies:
# - PHP
# - Uglifyjs (`npm install -g uglifyjs`)
# - Lessc (`npm install -g less`)
# - Lessc minifier (`npm install -g less-plugin-clean-css`)
# - HTML minifier (`npm install -g html-minifier`)
echo "Compressing/minifying web resources...\n";
if (is_dir('clientpack')) {
`rm -r clientpack`;
}
`cp -r client clientpack`;
// Toggle IIFE on
`sed -i -re 's~//IIFEMODE:~~g' clientpack/dcwebui.js`;
// Minify JS
`uglifyjs clientpack/dcwebui.js -o clientpack/dcwebui.min.js -c -m`;
// Minify CSS
`lessc --clean-css clientpack/dcwebui.css clientpack/dcwebui.min.css`;
// Embed css into HTML file
$html_content = file_get_contents('clientpack/index.htm');
$html_content = preg_replace_callback('~<link[^>]+dcwebui.css[^>]*>~', function() { return '<style type="text/css">'.file_get_contents('clientpack/dcwebui.min.css').'</style>'; }, $html_content);
// Embed JS into HTML file
$html_content = preg_replace_callback('~<script[^>]+dcwebui.js[^>]*>~', function() { return '<script type="text/javascript">'.file_get_contents('clientpack/dcwebui.min.js').'</script>'; }, $html_content);
// Embed socketio into HTML file
define('SIO_NAME', 'socket.io-1.7.2.js');
$html_content = preg_replace_callback('~<script[^>]+'.SIO_NAME.'[^>]*>~', function() { return '<script type="text/javascript">'.file_get_contents('clientpack/'.SIO_NAME).'</script>'; }, $html_content);
// Minify the combined file
file_put_contents('clientpack/index.htm', $html_content);
`html-minifier --collapse-whitespace -o clientpack/index.min.htm clientpack/index.htm`;
// Clean up files
`rm clientpack/{index.htm,dcwebui{.min,}.js,dcwebui{.min,}.css}`;
unlink('clientpack/'.SIO_NAME);
rename('clientpack/index.min.htm', 'clientpack/index.htm');

93
main.go
View File

@@ -12,10 +12,7 @@ import (
"github.com/googollee/go-socket.io"
)
type ActiveConnection struct {
s *socketio.Socket
h *libnmdc.HubConnection
}
var VERSION string = `nmdc-webfrontend/devel-unreleased`
type App struct {
cfg *Config
@@ -81,6 +78,29 @@ func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan stru
// Loop hub connection
serveUserInfo := func(nick string) {
props := ""
hub.Users(func(users *map[string]libnmdc.UserInfo) error {
uinfo, ok := (*users)[nick]
if !ok {
return nil // just skip
}
bProps, err := json.Marshal(uinfo)
if err != nil {
return nil // just skip
}
props = string(bProps)
return nil
})
// 'Message' is a json-encoded param with user properties
if len(props) > 0 {
so.Emit("info", UserMessageStruct{User: nick, Message: props})
}
}
for {
select {
@@ -100,14 +120,19 @@ func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan stru
case libnmdc.EVENT_PRIVATE:
so.Emit("priv", UserMessageStruct{User: hev.Nick, Message: hev.Message})
case libnmdc.EVENT_USER_UPDATED_INFO:
serveUserInfo(hev.Nick)
case libnmdc.EVENT_USER_JOINED:
so.Emit("join", UserMessageStruct{User: hev.Nick})
serveUserInfo(hev.Nick)
case libnmdc.EVENT_USER_PART:
so.Emit("part", UserMessageStruct{User: hev.Nick})
case libnmdc.EVENT_CONNECTION_STATE_CHANGED:
if hev.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED {
log.Printf("[%s] Connected to hub\n", so.Id())
so.Emit("hello")
} else if hev.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED {
so.Emit("close")
@@ -126,11 +151,6 @@ func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan stru
"raw": hev.UserCommand.Command,
})
default:
if this.cfg.App.Debug {
log.Printf("[%s] %v\n", so.Id(), hev)
}
}
case <-done:
@@ -146,6 +166,7 @@ func (this *App) SocketIOServer(so socketio.Socket) {
log.Printf("[%s] Client connected", so.Id())
so.Emit("cls")
so.Emit("hubname", this.cfg.Web.Title)
so.Emit("raw", this.cfg.App.MotdHTML+"<br>")
so.Emit("sys", "Enter a name to connect as (or name:pass for a registered nick)")
@@ -163,22 +184,8 @@ func (this *App) SocketIOServer(so socketio.Socket) {
}
func (this *App) ConfigRequestHandler(w http.ResponseWriter, r *http.Request) {
confStruct := struct {
Extern string `json:"extern"`
Title string `json:"title"`
}{
Extern: this.cfg.Web.Extern,
Title: this.cfg.Web.Title,
}
confBytes, _ := json.Marshal(confStruct)
//
w.Header().Set("Content-Type", "text/javascript")
w.WriteHeader(200)
fmt.Fprintf(w, "var DCWEBUI_CONF = %s;\n", string(confBytes))
func (this *App) customFaviconHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "favicon.ico")
}
func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) {
@@ -195,9 +202,9 @@ func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) {
knownContentTypes := map[string]string{
".htm": "text/html",
".css": "text/css",
".js": "application/javascript",
".png": "image/png",
".ico": "image/x-icon",
// No CSS/JS since they're embedded in the HTML
}
foundMime := false
@@ -219,6 +226,9 @@ func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) {
func (this *App) RunServer() {
// Inner mux {{
innerMux := http.NewServeMux()
// Socket.io handler
server, err := socketio.NewServer(nil)
if err != nil {
@@ -228,22 +238,39 @@ func (this *App) RunServer() {
server.On("error", func(so socketio.Socket, err error) {
log.Println("error:", err)
})
http.Handle("/socket.io/", server)
innerMux.Handle("/socket.io/", server)
// Configuration handler
http.HandleFunc("/conf", this.ConfigRequestHandler)
// Custom favicon handler
if this.cfg.Web.CustomFavicon {
innerMux.HandleFunc("/favicon.ico", this.customFaviconHandler)
}
// Other files: asset handler
http.HandleFunc("/", this.StaticRequestHandler)
// Asset handler
if this.cfg.Web.ExternalWebroot {
innerMux.Handle("/", http.FileServer(http.Dir("client")))
} else {
innerMux.HandleFunc("/", this.StaticRequestHandler)
}
// }}
// Wrapper mux {{
outerMux := http.NewServeMux()
outerMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", VERSION)
innerMux.ServeHTTP(w, r)
})
// }}
// Listen and serve
bindAddr := fmt.Sprintf("%s:%d", this.cfg.Web.BindTo, this.cfg.Web.Port)
log.Printf("Serving at %s...", bindAddr)
log.Fatal(http.ListenAndServe(bindAddr, nil))
log.Fatal(http.ListenAndServe(bindAddr, outerMux))
}
func main() {
log.Println(VERSION)
a, err := NewApp("nmdc-webfrontend.conf")
if err != nil {
log.Fatal(err.Error())

View File

@@ -1,23 +1,20 @@
{
"app": {
"name" : "DCWebUI2",
"version": "1.3.0",
"motd" : "Welcome!<br>",
"debug" : true
"motd": "Welcome!<br>"
},
"web": {
"port" : 8082,
"port": 8082,
"bind_to": "127.0.0.1",
"extern" : "http://127.0.0.1:8082",
"title" : "DCWebUI"
"title": "NMDC Web Frontend",
"custom_favicon": false
},
"hub": {
"address": "127.0.0.1",
"port" : 411,
"tag" : "nmdc-webfrontend"
"port": 411,
"tag": "nmdc-webfrontend"
}
}