text: (function () {
      var chosenServer = IO_SERVERS[0]; // Is the array even necessary for the ACP?
  
      var opts = {
          secure: chosenServer.secure
      };
  
      window.socket = io.connect(chosenServer.url, opts);
  
      window.socket.on("connect", function () {
          window.socket.emit("initACP");
          window.socket.emit("acp-list-activechannels");
          readEventlog();
      });
  
      window.socket.on("errMessage", function (data) {
          alert(data.msg);
      });
  })();
  
  function addMenuItem(target, text) {
      var ul = $("#nav-acp-section ul");
      var li = $("<li/>").appendTo(ul);
      var a = $("<a/>").attr("href", "javascript:void(0)")
          .text(text)
          .appendTo(li)
          .click(function () {
              $(".acp-panel").hide();
              $(target).show();
          });
  };
  
  addMenuItem("#acp-logview", "Log Viewer");
  addMenuItem("#acp-announcements", "Announcements");
  addMenuItem("#acp-global-bans", "Global Bans");
  addMenuItem("#acp-user-lookup", "Users");
  addMenuItem("#acp-channel-lookup", "Channels");
  addMenuItem("#acp-loaded-channels", "Active Channels");
  addMenuItem("#acp-eventlog", "Event Log");
  
  /* Log Viewer */
  function readSyslog() {
      $.ajax(location.protocol + "//" + location.host + "/acp/syslog").done(function (data) {
          $("#acp-log").text(data);
          $("#acp-log").scrollTop($("#acp-log").prop("scrollHeight"));
      });
  }
  
  function readErrlog() {
      $.ajax(location.protocol + "//" + location.host + "/acp/errlog").done(function (data) {
          $("#acp-log").text(data);
          $("#acp-log").scrollTop($("#acp-log").prop("scrollHeight"));
      });
  }
  
  function readHttplog() {
      $.ajax(location.protocol + "//" + location.host + "/acp/httplog").done(function (data) {
          $("#acp-log").text(data);
          $("#acp-log").scrollTop($("#acp-log").prop("scrollHeight"));
      });
  }
  
  function readEventlog() {
      $.ajax(location.protocol + "//" + location.host + "/acp/eventlog").done(function (data) {
          handleEventLog(data);
      });
  }
  
  function readChanlog(name) {
      $.ajax(location.protocol + "//" + location.host + "/acp/chanlog/" + name).done(function (data) {
          $("#acp-log").text(data);
          $("#acp-log").scrollTop($("#acp-log").prop("scrollHeight"));
      });
  }
  
  $("#acp-syslog-btn").click(readSyslog);
  $("#acp-errlog-btn").click(readErrlog);
  $("#acp-httplog-btn").click(readHttplog);
  $("#acp-chanlog-name").keyup(function (ev) {
      if (ev.keyCode === 13) {
          readChanlog($("#acp-chanlog-name").val());
      }
  });
  
  /* Announcements */
  $("#acp-announce-submit").click(function () {
      socket.emit("acp-announce", {
          title: $("#acp-announce-title").val(),
          content: $("#acp-announce-content").val()
      });
  });
  
  socket.on("announcement", function (data) {
      $("#acp-announcements").find(".announcement").remove();
  
      var signature = "<br>\u2014" + data.from;
      var al = makeAlert(data.title, data.text + signature)
          .removeClass("col-md-12")
          .addClass("announcement")
          .insertAfter($("#acp-announcements h3")[0]);
  
      al.find(".close").click(function () {
          socket.emit("acp-announce-clear");
      });
  
      $("#acp-announce-title").val(data.title);
      $("#acp-announce-content").val(data.text);
  });
  
  /* Global bans */
  $("#acp-gban-submit").click(function () {
      socket.emit("acp-gban", {
          ip: $("#acp-gban-ip").val(),
          note: $("#acp-gban-note").val()
      });
  });
  
  socket.on("acp-gbanlist", function (bans) {
      var tbl = $("#acp-global-bans table");
      tbl.find("tbody").remove();
  
      bans.forEach(function (b) {
          var tr = $("<tr/>").appendTo(tbl);
          var td = $("<td/>").appendTo(tr);
          var del = $("<button/>").addClass("btn btn-xs btn-danger")
              .html("<span class='glyphicon glyphicon-trash'></span>")
              .click(function () {
                  socket.emit("acp-gban-delete", b);
              })
              .appendTo(td);
  
          td = $("<td/>").appendTo(tr).html("<code>" + b.ip + "</code>");
          td = $("<td/>").appendTo(tr).text(b.note);
      });
  });
  
  /* User listing */
  (function () {
      var doSearch = function () {
          if ($("#acp-ulookup-query").val().trim() === "") {
              if (!confirm("You are about to list the entire users table. " +
                           "This table might be very large and take a long " +
                           "time to query.  Continue?")) {
                  return;
              }
          }
          socket.emit("acp-list-users", {
              value: $("#acp-ulookup-query").val(),
              field: $(this).data()["field"]
          });
      };
  
      $("#acp-ulookup-btn-name").click(doSearch);
      $("#acp-ulookup-btn-email").click(doSearch);
      $("#acp-ulookup-query").keyup(function (ev) {
          if (ev.keyCode === 13) {
              $("#acp-ulookup-btn-name").click();
          }
      });
  })();
  
  socket.on("acp-list-users", function (users) {
      var tbl = $("#acp-user-lookup table");
      tbl.data("entries", users);
      var p = tbl.data("paginator");
      if (p) {
          p.paginator.remove();
      }
  
      var opts = {
          preLoadPage: function () {
              tbl.find("tbody").remove();
          },
  
          generator: function (u, page, index) {
              var tr = $("<tr/>").appendTo(tbl);
              tr.attr("title", u.name + " joined on " + new Date(u.time) + " from IP " + u.ip);
              $("<td/>").text(u.id).appendTo(tr);
              $("<td/>").text(u.name).appendTo(tr);
              var rank = $("<td/>").text(u.global_rank).appendTo(tr);
              $("<td/>").text(u.email).appendTo(tr);
              var reset = $("<td/>").appendTo(tr);
  
              // Rank editor
              rank.click(function () {
                  if (rank.find(".rank-edit").length > 0) {
                      return;
                  }
  
                  var old = rank.text();
                  rank.text("");
  
                  var editor = $("<input/>").addClass("rank-edit form-control")
                      .attr("type", "text")
                      .attr("placeholder", old)
                      .appendTo(rank)
                      .focus();
  
                  var save = function () {
                      var newrank = editor.val();
                      if (newrank.trim() === "") {
                          newrank = old;
                      }
  
                      rank.text(old);
                      if (newrank === old) {
                          return;
                      }
  
                      socket.emit("acp-set-rank", {
                          name: u.name,
                          rank: parseInt(newrank)
                      });
  
                  };
  
                  editor.blur(save);
                  editor.keydown(function (ev) {
                      if (ev.keyCode === 13) {
                          save();
                      }
                  });
              });
  
              // Password reset
              $("<button/>").addClass("btn btn-xs btn-danger")
                  .text("Reset password")
                  .click(function () {
                      if (!confirm("Really reset password for " + u.name + "?")) {
                          return;
                      }
                      socket.emit("acp-reset-password", {
                          name: u.name,
                          email: u.email
                      }, function (result) {
                          if (result.error) {
                              modalAlert({
                                  title: "Error",
                                  textContent: result.error
                              });
                          } else {
                              var link = new URL("/account/passwordrecover/" + result.hash,
                                      new URL(location));
                              modalAlert({
                                  title: "Reset Link",
                                  textContent: link
                              });
                          }
                      });
                  }).appendTo(reset);
          }
      };
  
      p = Paginate(users, opts);
      p.paginator.css("margin-top", "20px");
      p.paginator.insertBefore(tbl);
      tbl.data("paginator", p);
  });
  
  socket.on("acp-set-rank", function (data) {
      var table = $("#acp-user-lookup table");
      var p = table.data("paginator");
      var e = table.data("entries");
      if (e) {
          for (var i = 0; i < e.length; i++) {
              if (e[i].name === data.name) {
                  e[i].rank = data.rank;
                  break;
              }
  
          }
          if (p) {
              p.items = e;
          }
      }
  
      table.find("td:contains('" + data.name + "')")
          .parent()
          .children()[2]
          .innerHTML = data.rank;
  });
  
  /* Channel listing */
  (function () {
      var doSearch = function () {
          if ($("#acp-clookup-value").val().trim() === "") {
              if (!confirm("You are about to list the entire channels table. " +
                           "This table might be very large and take a long " +
                           "time to query.  Continue?")) {
                  return;
              }
          }
          socket.emit("acp-list-channels", {
              field: $("#acp-clookup-field").val(),
              value: $("#acp-clookup-value").val()
          });
      };
  
      $("#acp-clookup-submit").click(doSearch);
      $("#acp-clookup-value").keyup(function (ev) {
          if (ev.keyCode === 13) {
              doSearch();
          }
      });
  })();
  
  socket.on("acp-list-channels", function (channels) {
      var tbl = $("#acp-channel-lookup table");
      tbl.data("entries", channels);
      var p = tbl.data("paginator");
      if (p) {
          p.paginator.remove();
      }
  
      var opts = {
          preLoadPage: function () {
              tbl.find("tbody").remove();
          },
  
          generator: function (c, page, index) {
              var tr = $("<tr/>").appendTo(tbl);
              tr.attr("title", c.name + " was registered on " + new Date(c.time));
              $("<td/>").text(c.id).appendTo(tr);
              $("<td/>").text(c.name).appendTo(tr);
              $("<td/>").text(c.owner).appendTo(tr);
              var remove = $("<td/>").appendTo(tr);
  
              // Drop channel
              $("<button/>").addClass("btn btn-xs btn-danger")
                  .text("Delete channel")
                  .click(function () {
                      if (!confirm("Really delete " + c.owner + "/" + c.name + "?")) {
                          return;
                      }
                      socket.emit("acp-delete-channel", {
                          name: c.name,
                      });
                  }).appendTo(remove);
          }
      };
  
      p = Paginate(channels, opts);
      p.paginator.css("margin-top", "20px");
      p.paginator.insertBefore(tbl);
      tbl.data("paginator", p);
  });
  
  socket.on("acp-delete-channel", function (data) {
      var table = $("#acp-channel-lookup table");
      var p = table.data("paginator");
      var e = table.data("entries");
      var found = -1;
      if (e) {
          for (var i = 0; i < e.length; i++) {
              if (e[i].name === data.name) {
                  found = i;
                  break;
              }
  
          }
  
          if (found > 0) {
              e.splice(found, 1);
          }
  
          if (p) {
              p.items = e;
          }
      }
  
      table.find("td:contains('" + data.name + "')")
          .parent()
          .remove();
  });
  
  /* Active channels */
  
  function showChannelDetailModal(c) {
      var wrap = $("<div/>").addClass("modal fade").appendTo($("body"));
      var dialog = $("<div/>").addClass("modal-dialog").appendTo(wrap);
      var content = $("<div/>").addClass("modal-content").appendTo(dialog);
      var head = $("<div/>").addClass("modal-header").appendTo(content);
      $("<button/>").addClass("close")
          .attr("data-dismiss", "modal")
          .attr("data-hidden", "true")
          .html("&times;")
          .appendTo(head);
      $("<h4/>").addClass("modal-title").text(c.name).appendTo(head);
  
      var body = $("<div/>").addClass("modal-body").appendTo(content);
  
      var table = $("<table/>").addClass("table table-striped table-compact")
          .appendTo(body);
      var tr;
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Page Title").appendTo(tr);
      $("<td/>").text(c.pagetitle).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Current Media").appendTo(tr);
      $("<a/>").attr("href", c.mediaLink).text(c.mediatitle).appendTo(
          $("<td/>").appendTo(tr)
      );
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("User Count").appendTo(tr);
      $("<td/>").text(c.usercount).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("User List").appendTo(tr);
      $("<td/>").text(c.users.join(" ")).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Registered").appendTo(tr);
      $("<td/>").text(c.registered).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Public").appendTo(tr);
      $("<td/>").text(c.public).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("ActiveLock Count").appendTo(tr);
      $("<td/>").text(c.activeLockCount).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Chat Filter Count").appendTo(tr);
      $("<td/>").text(c.chatFilterCount).appendTo(tr);
  
      tr = $("<tr/>").appendTo(table);
      $("<td/>").text("Emote Count").appendTo(tr);
      $("<td/>").text(c.emoteCount).appendTo(tr);
  
      $("<h3/>").text("Recent Chat").appendTo(body);
      $("<pre/>").text(c.chat.map(function (data) {
          var msg = "<" + data.username;
          if (data.addClass) {
              msg += "." + data.addClass;
          }
          msg += "> " + data.msg;
  
          msg = "[" + new Date(data.time).toTimeString().split(" ")[0] + "] " + msg;
          return msg;
      }).join("\n")).appendTo(body);
  
      wrap.on("hidden.bs.modal", function () {
          wrap.remove();
      });
  
      wrap.modal();
  }
  
  socket.on("acp-list-activechannels", function (channels) {
  console.log(channels[0]);
      var tbl = $("#acp-loaded-channels table");
      tbl.find("tbody").remove();
  
      channels.sort(function (a, b) {
          if (a.usercount === b.usercount) {
              var x = a.name.toLowerCase();
              var y = b.name.toLowerCase();
              return x === y ? 0 : (x > y ? 1 : -1);
          }
  
          return a.usercount > b.usercount ? -1 : 1;
      });
  
      var count = 0;
      channels.forEach(function (c) {
          var tr = $("<tr/>").appendTo(tbl);
          var name = $("<td/>").appendTo(tr);
          $("<a/>").attr("href", `/${CHANNELPATH}/${c.name}`)
              .text(c.pagetitle + ` (/${CHANNELPATH}/${c.name})`)
              .appendTo(name);
          var usercount = $("<td/>").text(c.usercount).appendTo(tr);
          count += c.usercount;
          var nowplaying = $("<td/>").text(c.mediatitle).appendTo(tr);
          var registered = $("<td/>").text(c.registered).appendTo(tr);
          var public = $("<td/>").text(c.public).appendTo(tr);
          var controlOuter = $("<td/>").appendTo(tr);
          var controlInner = $("<div/>").addClass("btn-group").appendTo(controlOuter);
          $("<button/>").addClass("btn btn-default btn-xs")
              .html("<span class='glyphicon glyphicon-list-alt'></span>")//.text("Details")
              .attr("title", "Details")
              .appendTo(controlInner)
              .click(function () {
                  showChannelDetailModal(c);
              });
          $("<button/>").addClass("btn btn-danger btn-xs")
              .html("<span class='glyphicon glyphicon-remove'></span>")//.text("Force Unload")
              .attr("title", "Unload")
              .appendTo(controlInner)
              .click(function () {
                  if (confirm(`Are you sure you want to unload /${CHANNELPATH}/${c.name}?`)) {
                      socket.emit("acp-force-unload", {
                          name: c.name
                      });
                  }
              });
      });
  
      var total = $("<tr/>").appendTo(tbl);
      $("<td/>").html("<strong>Total</strong>").appendTo(total);
      $("<td/>").html("<strong>" + count + "</strong>").appendTo(total);
      $("<td/>").appendTo(total);
      $("<td/>").appendTo(total);
      $("<td/>").appendTo(total);
      $("<td/>").appendTo(total);
  });
  
  $("#acp-lchannels-refresh").click(function () {
      socket.emit("acp-list-activechannels");
  });
  
  /* Event log */
  
  function getEventKey(line) {
      var left = line.indexOf("[", 1);
      var right = line.indexOf("]", left);
      return line.substring(left+1, right);
  }
  
  function handleEventLog(data) {
      data = data.split("\n").filter(function (ln) { return ln.indexOf("[") === 0; });
      var keys = {};
      data.forEach(function (ln) {
          keys[getEventKey(ln)] = true;
      });
  
      $("#acp-eventlog-text").data("lines", data);
  
      $("#acp-eventlog-filter").html("");
      for (var k in keys) {
          $("<option/>").attr("value", k)
              .text(k)
              .appendTo($("#acp-eventlog-filter"));
      }
  
      filterEventLog();
  }
  
  function filterEventLog() {
      var selected = $("#acp-eventlog-filter").val();
      var all = selected == null || selected.length === 0;
      var lines = $("#acp-eventlog-text").data("lines");
      var show = [];
      lines.forEach(function (ln) {
          if (all || selected.indexOf(getEventKey(ln)) !== -1) {
              show.push(ln);
          }
      });
  
      $("#acp-eventlog-text").text(show.join("\n"));
      $("#acp-eventlog-text").scrollTop($("#acp-eventlog-text").prop("scrollHeight"));
  }
  
  $("#acp-eventlog-filter").change(filterEventLog);
  $("#acp-eventlog-refresh").click(readEventlog);
  
  /* Initialize keyed table sorts */
  $("table").each(function () {
      var table = $(this);
      var sortable = table.find("th.sort");
      sortable.each(function () {
          var th = $(this);
          th.click(function () {
              var p = table.data("paginator");
              if (!p) {
                  return;
              }
              var key = th.attr("data-key");
              if (!key) {
                  return;
              }
              var asc = -th.attr("data-sort-direction") || -1;
              th.attr("data-sort-direction", asc);
              var entries = table.data("entries") || [];
              entries.sort(function (a, b) {
                  return a[key] === b[key] ? 0 : asc*(a[key] > b[key] ? 1 : -1);
              });
              table.data("entries", entries);
              p.items = entries;
              p.loadPage(0);
          });
      });
  });
  