You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@community.apache.org by hu...@apache.org on 2017/06/18 06:59:02 UTC

svn commit: r1799040 [2/2] - in /comdev/reporter.apache.org/trunk/site: css/tabs.css js/coffee/ js/coffee/html.coffee js/coffee/main.coffee js/coffee/misc.coffee js/coffee/preload.coffee js/coffee/tabs.coffee js/tabs.js ng.html

Added: comdev/reporter.apache.org/trunk/site/js/tabs.js
URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/tabs.js?rev=1799040&view=auto
==============================================================================
--- comdev/reporter.apache.org/trunk/site/js/tabs.js (added)
+++ comdev/reporter.apache.org/trunk/site/js/tabs.js Sun Jun 18 06:59:01 2017
@@ -0,0 +1,1212 @@
+// Generated by CoffeeScript 1.9.3
+var HTML, PMCchanges, addLine, addRelease, addTab, animals, buildPanel, currentTab, epochSecsYYYYMMDD, everyMonth, fetch, fetchJIRA, formatRm, getWednesdays, isArray, isHash, isNewPMC, jsdata, loadBread, loadTabs, m, makeSelect, mergeData, nproject, post, postJSON, preloadTabs, renderBZ, renderChart, renderFrontPage, renderJIRA, renderReleaseChart, setReportDate, tabs, templates, toInt, txt,
+  indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+HTML = (function() {
+  function HTML(type, params, children) {
+
+    /* create the raw element, or clone if passed an existing element */
+    var child, j, key, len1, subkey, subval, val;
+    if (typeof type === 'object') {
+      this.element = type.cloneNode();
+    } else {
+      this.element = document.createElement(type);
+    }
+
+    /* If params have been passed, set them */
+    if (isHash(params)) {
+      for (key in params) {
+        val = params[key];
+
+        /* Standard string value? */
+        if (typeof val === "string" || typeof val === 'number') {
+          this.element.setAttribute(key, val);
+        } else if (isArray(val)) {
+
+          /* Are we passing a list of data to set? concatenate then */
+          this.element.setAttribute(key, val.join(" "));
+        } else if (isHash(val)) {
+
+          /* Are we trying to set multiple sub elements, like a style? */
+          for (subkey in val) {
+            subval = val[subkey];
+            if (!this.element[key]) {
+              throw "No such attribute, " + key + "!";
+            }
+            this.element[key][subkey] = subval;
+          }
+        }
+      }
+    }
+
+    /* If any children have been passed, add them to the element */
+    if (children) {
+
+      /* If string, convert to textNode using txt() */
+      if (typeof children === "string") {
+        this.element.inject(txt(children));
+      } else {
+
+        /* If children is an array of elems, iterate and add */
+        if (isArray(children)) {
+          for (j = 0, len1 = children.length; j < len1; j++) {
+            child = children[j];
+
+            /* String? Convert via txt() then */
+            if (typeof child === "string") {
+              this.element.inject(txt(child));
+            } else {
+
+              /* Plain element, add normally */
+              this.element.inject(child);
+            }
+          }
+        } else {
+
+          /* Just a single element, add it */
+          this.element.inject(children);
+        }
+      }
+    }
+    return this.element;
+  }
+
+  return HTML;
+
+})();
+
+
+/**
+ * prototype injector for HTML elements:
+ * Example: mydiv.inject(otherdiv)
+ */
+
+HTMLElement.prototype.inject = function(child) {
+  var item, j, len1;
+  if (isArray(child)) {
+    for (j = 0, len1 = child.length; j < len1; j++) {
+      item = child[j];
+      if (typeof item === 'string') {
+        item = txt(item);
+      }
+      this.appendChild(item);
+    }
+  } else {
+    if (typeof child === 'string') {
+      child = txt(child);
+    }
+    this.appendChild(child);
+  }
+  return child;
+};
+
+txt = function(a) {
+  return document.createTextNode(a);
+};
+
+jsdata = {};
+
+templates = {};
+
+nproject = null;
+
+animals = ['hedgehogs', 'cows', 'geese', 'pigs', 'fluffy kittens', 'puppies', 'rabid dogs', 'ponies', 'weevils'];
+
+toInt = function(number) {
+  return number | 0;
+};
+
+makeSelect = function(name, arr) {
+  var j, len1, opt, sel, val;
+  sel = new HTML('select', {
+    name: name
+  });
+  for (j = 0, len1 = arr.length; j < len1; j++) {
+    val = arr[j];
+    opt = new HTML('option', {
+      value: val
+    });
+    opt.inject(val);
+    sel.inject(opt);
+  }
+  return sel;
+};
+
+getWednesdays = function(mo, y) {
+  var d, month, wednesdays;
+  d = new Date();
+  if (mo) {
+    d.setMonth(mo);
+  }
+  if (y) {
+    d.setFullYear(y, d.getMonth(), d.getDate());
+  }
+  month = d.getMonth();
+  wednesdays = [];
+  d.setDate(1);
+  while (d.getDay() !== 3) {
+    d.setDate(d.getDate() + 1);
+  }
+  while (d.getMonth() === month) {
+    wednesdays.push(new Date(d.getTime()));
+    d.setDate(d.getDate() + 7);
+  }
+  return wednesdays;
+};
+
+everyMonth = function(s) {
+  if ((indexOf.call(s, 'Next month') < 0)) {
+    return true;
+  }
+  return s === 'Every month';
+};
+
+m = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+formatRm = function(array) {
+  var first;
+  first = array[0];
+  if (array.length === 1) {
+    return first;
+  }
+  if (indexOf.call(m, first) < 0) {
+    return first.concat('; (default: ', array.slice(1).join(', '), ')');
+  }
+  return array.join(', ');
+};
+
+setReportDate = function(json, x) {
+  var dates, fullname, i, j, l, len1, len2, len3, len4, len5, link, nextdate, ny, o, pmc, q, r, ref, ref1, ref2, reportdate, rm, sm, today;
+  pmc = x[0];
+  reportdate = x[1];
+  fullname = (x[2] || "Unknown").replace(/Apache /, "");
+  today = new Date();
+  dates = [];
+  if (ref = !pmc, indexOf.call(json, ref) >= 0) {
+    pmc = fullname;
+  }
+  rm = json[pmc];
+  ref1 = json[pmc];
+  for (j = 0, len1 = ref1.length; j < len1; j++) {
+    i = ref1[j];
+    sm = json[pmc][i];
+    if (everyMonth(sm)) {
+      rm = m;
+      break;
+    }
+  }
+  for (l = 0, len2 = m.length; l < len2; l++) {
+    x = m[l];
+    for (o = 0, len3 = rm.length; o < len3; o++) {
+      i = rm[o];
+      if (m[x] === rm[i]) {
+        dates.push(getWednesdays(x)[2]);
+      }
+    }
+  }
+  ny = today.getFullYear() + 1;
+  for (q = 0, len4 = m.length; q < len4; q++) {
+    x = m[q];
+    for (r = 0, len5 = rm.length; r < len5; r++) {
+      i = rm[r];
+      if (m[x] === rm[i]) {
+        dates.push(getWednesdays(x, ny)[2]);
+      }
+    }
+  }
+  nextdate = dates[0];
+  while (nextdate < today) {
+    nextdate = dates.shift();
+  }
+  reportdate.innerHTML += "<b>Reporting schedule:</b> " + (json[pmc] ? formatRm(json[pmc]) : "Unknown(?)") + "<br>";
+  reportdate.innerHTML += "<b>Next report date: " + (nextdate ? nextdate.toDateString() : "Unknown(?)") + "</b>";
+  if (nextdate) {
+    link = "https://svn.apache.org/repos/private/foundation/board/board_agenda_" + nextdate.getFullYear() + "_" + ((ref2 = nextdate.getMonth() < 9) != null ? ref2 : {
+      "0": ""
+    }) + (nextdate.getMonth() + 1) + "_" + nextdate.getDate() + ".txt";
+    return reportdate.innerHTML += "<br>File your report in <a href='" + link + "'>" + link + "</a> when it has been seeded.";
+  }
+};
+
+buildPanel = function(pmc, title) {
+  var div, li, linkname, parent, titlebox, toc;
+  parent = document.getElementById('tab_' + pmc);
+  toc = document.getElementById('toc_' + pmc);
+  if (!toc) {
+    toc = document.createElement('cl');
+    toc.setAttribute("class", "sub-nav");
+    toc.setAttribute("id", "toc_" + pmc);
+    if (parent.firstChild.nextSibling) {
+      parent.insertBefore(toc, parent.firstChild.nextSibling);
+    } else {
+      parent.appendChild(toc);
+    }
+  }
+  linkname = title.toLowerCase().replace(/[^a-z0-9]+/, "");
+  li = document.createElement('dd');
+  li.setAttribute("role", "menu-item");
+  li.innerHTML = "<a href='#" + linkname + "_" + pmc + "'>" + title + "</a>";
+  toc.appendChild(li);
+  div = document.createElement('div');
+  div.setAttribute("id", linkname + "_" + pmc);
+  parent.appendChild(div);
+  titlebox = document.createElement('div');
+  titlebox.innerHTML = "<h3 style='background: #666; color: #EEE; border: 1px solid #66A; margin-top: 30px;'>" + title + " &nbsp; &nbsp; <small> <b>&uarr;</b> <a href='#tab_" + pmc + "'>Back to top</a></small></h3>";
+  div.appendChild(titlebox);
+  return div;
+};
+
+addLine = function(pmc, line) {
+  var i, j, l, len, len1, len2, lines, out, ref, ref1, results, word, words, xline;
+  line = line ? line : "  ";
+  lines = line.split(/\n/);
+  results = [];
+  for (j = 0, len1 = lines.length; j < len1; j++) {
+    xline = lines[j];
+    words = xline.split(" ");
+    len = 0;
+    out = "";
+    for (i = l = 0, len2 = words.length; l < len2; i = ++l) {
+      word = words[i];
+      len += word.replace(/<.+?>/, "").length + ((ref = i === words.length - 1) != null ? ref : {
+        0: 1
+      });
+      if (len >= 78) {
+        out += "\n   ";
+        len = words[i].replace(/<.+?>/, "").length + ((ref1 = i === words.length - 1) != null ? ref1 : {
+          0: 1
+        });
+      }
+      out += words[i] + " ";
+    }
+    results.push(templates[pmc] += out + "\n");
+  }
+  return results;
+};
+
+isNewPMC = function(json, pmc, after) {
+  return json.pmcdates[pmc].pmc[1] >= (after.getTime() / 1000);
+};
+
+PMCchanges = function(json, pmc, after) {
+  var afterTime, c, changes, entry, k, nc, ncn, np, npmc, npn, pmcStartTime, ref, roster;
+  changes = buildPanel(pmc, "PMC changes (from committee-info)");
+  roster = json.pmcdates[pmc].roster;
+  nc = 0;
+  np = 0;
+  ncn = null;
+  npn = null;
+  afterTime = after.getTime() / 1000;
+  pmcStartTime = json.pmcdates[pmc].pmc[2];
+  addLine(pmc, "## PMC changes:");
+  addLine(pmc);
+  if (pmcStartTime > afterTime) {
+    afterTime = pmcStartTime;
+    changes.innerHTML += "<h5>Changes since PMC creation:</h5>";
+  } else {
+    changes.innerHTML += "<h5>Changes within the last 3 months:</h5>";
+  }
+  c = 0;
+  npmc = 0;
+  for (k in roster) {
+    entry = roster[k];
+    c++;
+    if (entry[1] > afterTime) {
+      npmc++;
+    }
+  }
+  addLine(pmc, " - Currently " + c + " PMC members.");
+  if (npmc > 1) {
+    addLine(pmc, " - New PMC members:");
+  }
+  for (k in roster) {
+    entry = roster[k];
+    if (entry[1] > np) {
+      np = entry[1];
+      npn = entry[0];
+    }
+    if (entry[1] > afterTime) {
+      changes.innerHTML += "&rarr; " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString() + "<br>";
+      addLine(pmc, ((ref = npmc > 1) != null ? ref : {
+        "   ": ""
+      }) + " - " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString());
+    }
+  }
+  if (npmc === 0) {
+    addLine(pmc, " - No new PMC members added in the last 3 months");
+    changes.innerHTML += "&rarr; <font color='red'><b>No new PMC members in the last 3 months.</b></font><br>";
+  }
+  if (npn) {
+    if (np < afterTime) {
+      addLine(pmc, " - Last PMC addition was " + npn + " on " + new Date(np * 1000).toDateString());
+    }
+    changes.innerHTML += "&rarr; " + "<b>Last PMC addition: </b>" + new Date(np * 1000).toDateString() + " (" + npn + ")<br>";
+  }
+  changes.innerHTML += "&rarr; " + "<b>Currently " + c + " PMC members.<br>";
+  changes.innerHTML += "<br>PMC established: " + json.pmcdates[pmc].pmc[0];
+  if (pmcStartTime > 0) {
+    changes.innerHTML += " (assumed actual date: " + epochSecsYYYYMMDD(pmcStartTime) + ")";
+  }
+  return addLine(pmc);
+};
+
+epochSecsYYYYMMDD = (function(_this) {
+  return function(t) {
+    return new Date(t * 1000).toISOString().slice(0, 10);
+  };
+})(this);
+
+renderFrontPage = function(tpmc) {
+  var a, add, after, c, changes, container, cu, d, date, dialog, diff, div, entry, err, f, first, hcolors, health, hvalues, i, j, json, k, l, len1, len2, lookup, lr, lrn, ml, mlbox, mlname, mo, nc, ncn, ncom, np, npmc, npn, nr, obj, p, pmc, prev, rdialog, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, releases, reportdate, results, skip, sproject, template, text, thisHour, title, top, total, tr, ul, version, x, xml, y;
+  thisHour = toInt(new Date().getTime() / (3600 * 1000));
+  container = document.getElementById('contents');
+  top = document.createElement('div');
+  container.appendChild(top);
+  json = jsdata;
+  sproject = tpmc;
+  hcolors = ["#000070", "#007000", "#407000", "#70500", "#700000", "#A00000"];
+  hvalues = ["Super Healthy", "Healthy", "Mostly Okay", "Unhealthy", "Action required!", "URGENT ACTION REQUIRED!"];
+  ref = json.pmcs;
+  results = [];
+  for (i = j = 0, len1 = ref.length; j < len1; i = ++j) {
+    pmc = ref[i];
+    if (pmc === tpmc || !tpmc) {
+      if ((ref1 = !pmc, indexOf.call(json.pmcdates, ref1) >= 0)) {
+        continue;
+      }
+      templates[pmc] = "";
+      addLine(pmc, "## Description:");
+      if ((indexOf.call(jsdata.pdata[pmc], 'shortdesc') >= 0)) {
+        addLine(pmc, "   " + json.pdata[pmc].shortdesc);
+      } else {
+        addLine(pmc, " - <font color='red'>Description goes here</font>");
+      }
+      addLine(pmc);
+      a = animals[Math.floor(Math.random() * animals.length * 0.999)];
+      addLine(pmc, "## Issues:");
+      addLine(pmc, " - <font color='red'>TODO - list any issues that require board attention, \n  or say \"there are no issues requiring board attention at this time\" - if not, the " + a + " will get you.</font>");
+      addLine(pmc);
+      addLine(pmc, "## Activity:");
+      addLine(pmc, " - <font color='red'>TODO - the PMC <b><u>MUST</u></b> provide this information</font>");
+      addLine(pmc);
+      a = animals[Math.floor(Math.random() * animals.length * 0.999)];
+      addLine(pmc, "## Health report:");
+      addLine(pmc, " - <font color='red'>TODO - Please use this paragraph to elaborate on why the current project activity (mails, commits, bugs etc) is at its current level - Maybe " + a + " took over and are now controlling the project?</font>");
+      addLine(pmc);
+      obj = document.createElement('div');
+      obj.setAttribute("id", "tab_" + pmc);
+      obj.style = "padding: 10px; text-align: left !important;";
+      obj.setAttribute("aria-hidden", "true");
+      title = document.createElement('h2');
+      title.innerHTML = json.pdata[pmc].name || pmc;
+      obj.appendChild(title);
+      health = document.createElement('p');
+      if (json.health[pmc] && !isNaN(json.health[pmc]['cscore'])) {
+        health.style.marginTop = "10px";
+        health.innerHTML = "<b>Committee Health score:</b> <a href='chi.py#" + pmc + "'><u><font color='" + hcolors[json.health[pmc]['cscore']] + "'>" + (6.33 + (json.health[pmc]['score'] * -1.00 * (20 / 12.25))).toFixed(2) + " (" + hvalues[json.health[pmc]['cscore']] + ")</u></font></a> <i>(This is an automatically generated score, it is NOT authoritative in any way!)</i>";
+        obj.appendChild(health);
+      }
+      container.appendChild(obj);
+      reportdate = buildPanel(pmc, "Report date");
+      if (json.pdata[pmc].chair) {
+        reportdate.innerHTML += "<b>Committee Chair: </b>" + json.pdata[pmc].chair + "<br>";
+      }
+      fetch("reportingcycles.json?" + thisHour, [pmc, reportdate, json.pdata[pmc].name], setReportDate);
+      mo = new Date().getMonth() - 3;
+      after = new Date();
+      after.setMonth(mo);
+      PMCchanges(json, pmc, after);
+      changes = buildPanel(pmc, "PMC changes (From LDAP)");
+      c = 0;
+      cu = 0;
+      ref2 = json.changes[pmc].committer;
+      for (x in ref2) {
+        y = ref2[x];
+        cu++;
+        c++;
+      }
+      ref3 = json.changes[pmc].pmc;
+      for (x in ref3) {
+        y = ref3[x];
+        c++;
+      }
+      nc = 0;
+      np = 0;
+      ncn = null;
+      npn = null;
+      addLine(pmc, "## Committer base changes:");
+      addLine(pmc);
+      addLine(pmc, " - Currently " + json.count[pmc][1] + " committers.");
+      if (cu === 0) {
+        if (isNewPMC(json, pmc, after)) {
+          addLine(pmc, " - No changes (the PMC was established in the last 3 months)");
+        } else {
+          addLine(pmc, " - No new changes to the committer base since last report.");
+        }
+        addLine(pmc);
+      }
+      if (c === 0) {
+        if (isNewPMC(json, pmc, after)) {
+          changes.innerHTML += "No changes - the PMC was established in the last 3 months.";
+        } else {
+          changes.innerHTML += "<font color='red'><b>No new changes to the PMC or committer base detected - (LDAP error or no changes for &gt;2 years)</b></font>";
+        }
+      } else {
+        changes.innerHTML += "<h5>Changes within the last 3 months:</h5>";
+        npmc = 0;
+        ref4 = json.changes[pmc].pmc;
+        for (k in ref4) {
+          entry = ref4[k];
+          if (entry[1] > after.getTime() / 1000) {
+            npmc++;
+          }
+        }
+        ref5 = json.changes[pmc].pmc;
+        for (k in ref5) {
+          entry = ref5[k];
+          if (entry[1] > np) {
+            np = entry[1];
+            npn = entry[0];
+          }
+          if (entry[1] > after.getTime() / 1000) {
+            changes.innerHTML += "&rarr; " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString() + "<br>";
+          }
+        }
+        if (npmc === 0) {
+          if (isNewPMC(json, pmc, after)) {
+            changes.innerHTML += "&rarr; No new PMC members in the 3 months since the PMC was established<br>";
+          } else {
+            changes.innerHTML += "&rarr; <font color='red'><b>No new PMC members in the last 3 months.</b></font><br>";
+          }
+        }
+        if (npn) {
+          changes.innerHTML += "&rarr; " + "<b>Last PMC addition: </b>" + new Date(np * 1000).toDateString() + " (" + npn + ")<br>";
+        }
+        ncom = 0;
+        ref6 = json.changes[pmc].committer;
+        for (k in ref6) {
+          entry = ref6[k];
+          if (entry[1] > after.getTime() / 1000) {
+            ncom++;
+          }
+        }
+        if (ncom > 1) {
+          addLine(pmc, " - New commmitters:");
+        }
+        ref7 = json.changes[pmc].committer;
+        for (k in ref7) {
+          entry = ref7[k];
+          if (entry[1] > nc) {
+            nc = entry[1];
+            ncn = entry[0];
+          }
+          if (entry[1] > after.getTime() / 1000) {
+            changes.innerHTML += "&rarr; " + entry[0] + " was added as a committer on " + new Date(entry[1] * 1000).toDateString() + "<br>";
+            addLine(pmc, ((ref8 = ncom > 1) != null ? ref8 : {
+              "   ": ""
+            }) + " - " + entry[0] + " was added as a committer on " + new Date(entry[1] * 1000).toDateString());
+          }
+        }
+        if (ncom === 0) {
+          changes.innerHTML += "&rarr; <font color='red'><b>No new committers in the last 3 months.</b></font><br>";
+          addLine(pmc, " - No new committers added in the last 3 months");
+        }
+        if (ncn) {
+          if (nc < after.getTime() / 1000) {
+            addLine(pmc, " - Last committer addition was " + ncn + " at " + new Date(nc * 1000).toDateString());
+          }
+          changes.innerHTML += "&rarr; " + "<b>Last committer addition: </b>" + new Date(nc * 1000).toDateString() + " (" + ncn + ")<br>";
+        } else {
+          addLine(pmc, " - Last committer addition was more than 2 years ago");
+          changes.innerHTML += "&rarr; " + "<b>Last committer addition: </b><font color='red'>more than two years ago (not in the archive!)</font><br>";
+        }
+        changes.innerHTML += "&rarr; " + "<b>Currently " + json.count[pmc][1] + " committers and " + json.count[pmc][0] + " PMC members.";
+        addLine(pmc);
+      }
+      releases = buildPanel(pmc, "Releases");
+      addLine(pmc, "## Releases:");
+      addLine(pmc);
+      nr = 0;
+      lr = null;
+      lrn = 0;
+      tr = 0;
+      ref9 = json.releases[pmc];
+      for (version in ref9) {
+        date = ref9[version];
+        tr++;
+        if (date > lrn) {
+          lrn = date;
+          lr = version;
+        }
+        if (date >= after.getTime() / 1000) {
+          err = "";
+          if (new Date(date * 1000) > new Date()) {
+            err = " (<font color='red'>This seems wrong?!</font>)";
+          }
+          releases.innerHTML += "&rarr; " + "<b>" + version + "</b> was released on " + new Date(date * 1000).toDateString() + err + "<br>";
+          addLine(pmc, " - " + version + " was released on " + new Date(date * 1000).toDateString() + err);
+          nr++;
+        }
+      }
+      if (nr === 0) {
+        if (lr) {
+          releases.innerHTML += "&rarr; " + "<b>Last release was " + lr + ", released on </b>" + new Date(lrn * 1000).toDateString() + "<br>";
+          addLine(pmc, " - Last release was " + lr + " on " + new Date(lrn * 1000).toDateString());
+          if (lr.match("incubat") && !isNewPMC(json, pmc, after)) {
+            releases.innerHTML += "<br><font color='red'><b>No release since graduation</b></font><br><br>";
+            addLine(pmc, " - <font color='red'><b>No release since graduation??? [FIX!]</b></font>");
+          }
+        } else {
+          releases.innerHTML += "No release data could be found.<br>";
+          addLine(pmc, " - <font color='red'>No release data could be found [FIX!]</font>");
+        }
+      }
+      releases.innerHTML += "<i>(A total of " + (tr - nr) + " older release(s) were found for " + pmc + " in our db)</i><br>";
+      releases.innerHTML += "<br><a href='javascript:void(0);' onclick=\"$('#rdialog_" + pmc + "').dialog({minWidth: 450, minHeight: 240});\">Add a release</a>";
+      releases.innerHTML += " - <a href='javascript:void(0);' onclick=\"$('#dialog_" + pmc + "').dialog({minWidth: 450, minHeight: 240});\">Fetch releases from JIRA</a>";
+      releases.innerHTML += " - <a href='addrelease.html?" + pmc + "'>Manage release versions</a><br>";
+      if (tr > 0) {
+        div = renderReleaseChart(json.releases[pmc], pmc, releases);
+        releases.appendChild(div);
+      }
+      addLine(pmc);
+      mlbox = buildPanel(pmc, "Mailing lists");
+      ul = document.createElement('ul');
+      ul.style.textAlign = "left;";
+      mlbox.appendChild(ul);
+      prev = "";
+      f = 0;
+      addLine(pmc, "## Mailing list activity:");
+      addLine(pmc);
+      addLine(pmc, " - <font color='red'>TODO Please explain what the following statistics mean for the project." + " If there is nothing significant in the figures, omit this section.</font>");
+      addLine(pmc);
+      first = ['users', 'dev', 'commits', 'private', 'bugs', 'modules-dev'];
+      for (i in first) {
+        x = first[i];
+        ml = pmc + ".apache.org-" + first[i];
+        if (ml !== prev && ml.search("infra") < 0 && json.mail[pmc] && json.mail[pmc][ml]) {
+          f++;
+          prev = ml;
+          d = ml.split(".org-");
+          mlname = d[1] + "@" + d[0] + ".org";
+          lookup = d[0].split(/\./)[0] + "-" + d[1];
+          x = renderChart(json.mail[pmc], ml, obj, json.delivery[pmc] && json.delivery[pmc][lookup] ? json.delivery[pmc][lookup].weekly : {});
+          total = x[0];
+          diff = x[1];
+          div = x[2];
+          add = "";
+          if (json.delivery[pmc] && json.delivery[pmc][lookup]) {
+            add = ":\n    - " + json.delivery[pmc][lookup].quarterly[0] + " emails sent to list (" + json.delivery[pmc][lookup].quarterly[1] + " in previous quarter)";
+          }
+          text = "Currently: " + total + " subscribers <font color='green'>(up " + diff + " in the last 3 months)</font>";
+          if (diff < 0) {
+            text = "Currently: " + total + " subscribers <font color='red'>(down " + diff + " in the last 3 months)</font>";
+            if (d[1] !== "private" && d[1] !== "security" && d[1] !== "commits") {
+              addLine(pmc, " - " + mlname + ": ");
+              addLine(pmc, "    - " + total + " subscribers (down " + diff + " in the last 3 months)" + add);
+              addLine(pmc);
+            }
+          } else {
+            if (d[1] !== "private" && d[1] !== "security" && d[1] !== "commits") {
+              addLine(pmc, " - " + mlname + ": ");
+              addLine(pmc, "    - " + total + " subscribers (up " + diff + " in the last 3 months)" + add);
+              addLine(pmc);
+            }
+          }
+          if (json.delivery[pmc] && json.delivery[pmc][lookup]) {
+            text += " (" + json.delivery[pmc][lookup].quarterly[0] + " emails sent in the past 3 months, " + json.delivery[pmc][lookup].quarterly[1] + " in the previous cycle)";
+          }
+          p = document.createElement('li');
+          p.innerHTML = "<h5>" + mlname + ":</h5>" + text;
+          p.appendChild(div);
+          ul.appendChild(p);
+        }
+      }
+      for (ml in json.mail[pmc]) {
+        skip = false;
+        for (l = 0, len2 = first.length; l < len2; l++) {
+          i = first[l];
+          xml = pmc + ".apache.org-" + first[i];
+          if (ml.search(xml) === 0) {
+            skip = true;
+          }
+        }
+        if (!skip) {
+          f++;
+          if (ml !== prev && ml.search("infra") < 0) {
+            prev = ml;
+            d = ml.split(".org-");
+            mlname = d[1] + "@" + d[0] + ".org";
+            lookup = d[0].split(/\./)[0] + "-" + d[1];
+            x = renderChart(json.mail[pmc], ml, obj, json.delivery[pmc] && json.delivery[pmc][lookup] ? json.delivery[pmc][lookup].weekly : {});
+            total = x[0];
+            diff = x[1];
+            div = x[2];
+            add = "";
+            if (json.delivery[pmc] && json.delivery[pmc][lookup]) {
+              add = ":\n    - " + json.delivery[pmc][lookup].quarterly[0] + " emails sent to list (" + json.delivery[pmc][lookup].quarterly[1] + " in previous quarter)";
+            }
+            text = "Currently: " + total + " subscribers <font color='green'>(up " + diff + " in the last 3 months)</font>";
+            if (diff < 0) {
+              text = "Currently: " + total + " subscribers <font color='red'>(down " + diff + " in the last 3 months)</font>";
+              if (d[1] !== "private" && d[1] !== "security" && d[1] !== "commits") {
+                addLine(pmc, " - " + mlname + ": ");
+                addLine(pmc, "    - " + total + " subscribers (down " + diff + " in the last 3 months)" + add);
+                addLine(pmc);
+              }
+            } else {
+              if (d[1] !== "private" && d[1] !== "security" && d[1] !== "commits") {
+                addLine(pmc, " - " + mlname + ": ");
+                addLine(pmc, "    - " + total + " subscribers (up " + diff + " in the last 3 months)" + add);
+                addLine(pmc);
+              }
+            }
+            if (json.delivery[pmc] && json.delivery[pmc][lookup]) {
+              text += " (" + json.delivery[pmc][lookup].quarterly[0] + " emails sent in the past 3 months, " + json.delivery[pmc][lookup].quarterly[1] + " in the previous cycle)";
+            }
+            p = document.createElement('li');
+            p.innerHTML = "<h5>" + mlname + ":</h5>" + text;
+            p.appendChild(div);
+            ul.appendChild(p);
+          }
+        }
+      }
+      addLine(pmc);
+      if (json.bugzilla[pmc][0] || json.bugzilla[pmc][1] > 0) {
+        renderBZ(pmc);
+      }
+      if (json.jira[pmc][0] > 0 || json.jira[pmc][1] > 0) {
+        renderJIRA(pmc);
+      }
+      template = buildPanel(pmc, "Report template");
+      template.innerHTML += "<pre style='border: 2px dotted #444; padding: 10px; background: #FFD;' contenteditable='true'>" + templates[pmc] + "</pre>";
+      dialog = document.createElement('div');
+      dialog.setAttribute("id", "dialog_" + pmc);
+      dialog.setAttribute("title", "Fetch data from JIRA for " + pmc);
+      dialog.setAttribute("style", "display: none;");
+      if (jsdata.keys[pmc] && jsdata.keys[pmc].length > 0) {
+        dialog.innerHTML = "<p>Suggested JIRA Keys: <kbd>" + jsdata.keys[pmc].join(", ") + "</kbd></p>";
+      } else {
+        dialog.innerHTML = "<p>No JIRA keys found - are you sure this project uses JIRA?</p>";
+      }
+      dialog.innerHTML += "<form><b>JIRA Project:</b><input type='text' name='jira' placeholder='FOO'><br><b>Optional prepend:</b> <input name='prepend' type='text' placeholder='Foo'/><br>" + "<input type='button' value='Fetch from JIRA' onclick='fetchJIRA(\"" + pmc + "\", this.form[\"jira\"].value, this.form[\"prepend\"].value);'></form>" + "<p>If you have multiple JIRA projects and they only have the version number in their release versions, please enter the component name in the 'prepend' field.</p>";
+      document.getElementById('tab_' + pmc).appendChild(dialog);
+      rdialog = document.createElement('div');
+      rdialog.setAttribute("id", "rdialog_" + pmc);
+      rdialog.setAttribute("title", "Add a release for " + pmc);
+      rdialog.setAttribute("style", "display: none;");
+      rdialog.innerHTML = "<form><b>Version:</b><input type='text' name='version' placeholder='1.2.3'><br>" + "<b>Date:</b> <input name='date' type='text' placeholder='YYYY-MM-DD'/><br>" + "<input type='button' value='Add release' onclick='addRelease(\"" + pmc + "\", this.form[\"version\"].value, this.form[\"date\"].value);'></form>";
+      results.push(container.appendChild(rdialog));
+    } else {
+      results.push(void 0);
+    }
+  }
+  return results;
+};
+
+renderJIRA = function(pmc) {
+  var obj;
+  obj = buildPanel(pmc, "JIRA Statistics");
+  addLine(pmc, "## JIRA activity:");
+  addLine(pmc);
+  addLine(pmc, " - " + jsdata.jira[pmc][0] + " JIRA tickets created in the last 3 months");
+  addLine(pmc, " - " + jsdata.jira[pmc][1] + " JIRA tickets closed/resolved in the last 3 months");
+  addLine(pmc);
+  obj.innerHTML += jsdata.jira[pmc][0] + " JIRA tickets created in the last 3 months<br>";
+  obj.innerHTML += jsdata.jira[pmc][1] + " JIRA tickets closed/resolved in the last 3 months<br>";
+  if (jsdata.keys[pmc]) {
+    obj.innerHTML += "Keys used: <kbd>" + jsdata.keys[pmc].join(", ") + "</kbd><br>";
+  }
+  return obj.innerHTML += "Keys with tickets: <kbd>" + jsdata.jira[pmc][2].join(", ") + "</kbd>";
+};
+
+renderBZ = function(pmc) {
+  var obj;
+  obj = buildPanel(pmc, "Bugzilla Statistics");
+  addLine(pmc, "## Bugzilla Statistics:");
+  addLine(pmc);
+  addLine(pmc, " - " + jsdata.bugzilla[pmc][0] + " Bugzilla tickets created in the last 3 months");
+  addLine(pmc, " - " + jsdata.bugzilla[pmc][1] + " Bugzilla tickets resolved in the last 3 months");
+  addLine(pmc);
+  obj.innerHTML += jsdata.bugzilla[pmc][0] + " Bugzilla tickets created in the last 3 months<br>";
+  obj.innerHTML += jsdata.bugzilla[pmc][1] + " Bugzilla tickets resolved in the last 3 months<br>";
+  return obj.innerHTML += "Tickets were found for the following products:<br><kbd>" + Object.keys(jsdata.bugzilla[pmc][2]).sort().join(", ") + "</kbd>";
+};
+
+renderChart = function(json, name, container, delivery) {
+  var chart, chartDiv, count, cu, d, data, date, dateString, dates, difference, dp, hitFirst, j, len1, mlname, narr, noweekly, odp, options, ref;
+  chartDiv = document.createElement('div');
+  chartDiv.setAttribute("id", name + "_chart");
+  dates = [];
+  noweekly = 0;
+  ref = json[name];
+  for (date in ref) {
+    count = ref[date];
+    dates.push(date);
+  }
+  for (date in delivery) {
+    count = delivery[date];
+    noweekly++;
+  }
+  d = name.split(".org-");
+  mlname = d[1] + "@" + d[0] + ".org";
+  dates.sort();
+  cu = 0;
+  narr = [];
+  hitFirst = false;
+  dp = new Date();
+  dp.setMonth(dp.getMonth() - 3);
+  odp = new Date();
+  odp.setMonth(odp.getMonth() - 6);
+  difference = 0;
+  for (j = 0, len1 = dates.length; j < len1; j++) {
+    date = dates[j];
+    dateString = new Date(parseInt(date) * 1000);
+    if (dateString > dp) {
+      difference += json[name][date];
+    }
+    cu = cu + json[name][date];
+    if (cu > 0) {
+      hitFirst = true;
+    }
+    if ((cu > 0 || hitFirst) && dateString >= odp) {
+      if (noweekly > 0) {
+        narr.push([dateString, cu, delivery[date] || 0]);
+      } else {
+        narr.push([dateString, cu]);
+      }
+    }
+  }
+  data = new google.visualization.DataTable();
+  data.addColumn('date', 'Date');
+  data.addColumn('number', "List members");
+  if (noweekly > 0) {
+    data.addColumn('number', "Emails sent per week");
+  }
+  data.addRows(narr);
+  options = {
+    title: 'Mailing list stats for ' + mlname,
+    backgroundColor: 'transparent',
+    width: 900,
+    height: 260,
+    legend: {
+      position: 'none',
+      maxLines: 3
+    },
+    vAxis: {
+      format: "#"
+    },
+    vAxes: noweekly > 0 ? [
+      {
+        title: 'Emails per week',
+        titleTextStyle: {
+          color: '#DD0000'
+        },
+        min: 0
+      }, {
+        title: 'Subscribers',
+        titleTextStyle: {
+          color: '#0000DD'
+        },
+        min: 0,
+        minValue: 0
+      }
+    ] : [
+      {
+        title: 'Subscribers',
+        titleTextStyle: {
+          color: '#0000DD'
+        }
+      }
+    ],
+    series: {
+      0: {
+        type: "line",
+        pointSize: 4,
+        lineWidth: 2,
+        targetAxisIndex: noweekly > 0 ? 1 : null
+      },
+      1: {
+        type: "bars",
+        targetAxisIndex: noweekly > 0 ? 0 : [0, 1]
+      }
+    },
+    seriesType: "bars",
+    tooltip: {
+      isHtml: true
+    }
+  };
+  chart = new google.visualization.ComboChart(chartDiv);
+  chart.draw(data, options);
+  return [cu, difference, chartDiv];
+};
+
+renderReleaseChart = function(releases, name, container) {
+  var chart, chartDiv, data, date, major, maxLen, narr, options, version, x;
+  chartDiv = null;
+  if (document.getElementById(name + "_releasechart")) {
+    chartDiv = document.getElementById(name + "_releasechart");
+  } else {
+    chartDiv = document.createElement('div');
+    chartDiv.setAttribute("id", name + "_releasechart");
+  }
+  narr = [];
+  maxLen = 1;
+  for (version in releases) {
+    date = releases[version];
+    x = version.match(/(\d+)\.(\d+)/);
+    if (x && x[2].length > maxLen) {
+      maxLen = x[2].length;
+    }
+  }
+  for (version in releases) {
+    date = releases[version];
+    if (new Date(releases[version] * 1000).getFullYear() >= 1999) {
+      major = parseFloat(version) || 1;
+      x = version.match(/(\d+)\.(\d+)/);
+      if (x) {
+        while (x[2].length < maxLen) {
+          x[2] = "0" + x[2];
+        }
+        major = parseFloat(x[1] + "." + x[2]);
+      }
+      narr.push([new Date(releases[version] * 1000), major, version + " - " + new Date(releases[version] * 1000).toDateString()]);
+    }
+  }
+  data = new google.visualization.DataTable();
+  data.addColumn('datetime', 'Date');
+  data.addColumn('number', 'Version');
+  data.addColumn('string', 'tooltip');
+  data.setColumnProperty(2, 'role', 'tooltip');
+  data.addRows(narr);
+  options = {
+    title: 'Release timeline for ' + name,
+    height: 240,
+    width: 800,
+    backgroundColor: 'transparent',
+    series: [
+      {
+        pointSize: 15
+      }
+    ],
+    pointShape: {
+      type: 'star',
+      sides: 5
+    }
+  };
+  chart = new google.visualization.ScatterChart(chartDiv);
+  chartDiv.style.marginLeft = "50px";
+  chart.draw(data, options);
+  return chartDiv;
+};
+
+fetchJIRA = function(pmc, project, prepend) {
+  if (project && project.length > 1) {
+    return fetch("jiraversions.py?project=" + pmc + "&jiraname=" + project + "&prepend=" + prepend, null, function(json) {
+      var j, len1, ref, version;
+      if (json && json.versions) {
+        ref = json.versions;
+        for (j = 0, len1 = ref.length; j < len1; j++) {
+          version = ref[j];
+          jsdata.releases[pmc][version] = json.versions[version];
+        }
+        $('#dialog_' + pmc).dialog("close");
+        nproject = pmc;
+        alert("Fetched " + json.added + " releases from JIRA!");
+        return renderFrontPage(jsdata);
+      } else if (json && json.status) {
+        return alert(json.status);
+      } else if (json) {
+        return alert(JSON.stringify(json));
+      } else {
+        return alert("Couldn't find any release data :(");
+      }
+    });
+  }
+};
+
+addRelease = function(pmc, version, date) {
+  var nn, now, x, y;
+  if (version && version.length > 1 && date.match(/^(\d\d\d\d)-(\d\d)-(\d\d)$/)) {
+    x = date.split("-");
+    y = new Date(x[0], parseInt(x[1]) - 1, parseInt(x[2]));
+    nn = parseInt(y.getTime() / 1000);
+    now = (new Date().getTime()) / 1000;
+    if (nn >= now) {
+      alert("Date is in the future!");
+      return;
+    }
+    return fetch("addrelease.py?json=true&committee=" + pmc + "&version=" + escape(version) + "&date=" + nn, null, function(json) {
+      var j, len1, n, ref;
+      if (json && json.versions) {
+        n = 0;
+        ref = json.versions;
+        for (j = 0, len1 = ref.length; j < len1; j++) {
+          version = ref[j];
+          n++;
+          jsdata.releases[pmc][version] = json.versions[version];
+        }
+        $('#rdialog_' + pmc).dialog("close");
+        nproject = pmc;
+        alert("Release added!");
+        return renderFrontPage(jsdata);
+      } else if (json && json.status) {
+        return alert(json.status);
+      } else if (json) {
+        return alert(JSON.stringify(json));
+      } else {
+        return alert("Couldn't add release data :(");
+      }
+    });
+  }
+};
+
+Number.prototype.pretty = function(fix) {
+  if (fix) {
+    return String(this.toFixed(fix)).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
+  }
+  return String(this.toFixed(0)).replace(/(\d)(?=(\d{3})+$)/g, '$1,');
+};
+
+fetch = function(url, xstate, callback, snap) {
+  var xmlHttp;
+  xmlHttp = null;
+  if (window.XMLHttpRequest) {
+    xmlHttp = new XMLHttpRequest();
+  } else {
+    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+  }
+  xmlHttp.withCredentials = true;
+  xmlHttp.open("GET", url, true);
+  xmlHttp.send(null);
+  return xmlHttp.onreadystatechange = function(state) {
+    var e, response;
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 500) {
+      if (snap) {
+        snap(xstate);
+      }
+    }
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
+      if (callback) {
+        try {
+          response = JSON.parse(xmlHttp.responseText);
+          return callback(response, xstate);
+        } catch (_error) {
+          e = _error;
+          return callback(JSON.parse(xmlHttp.responseText), xstate);
+        }
+      }
+    }
+  };
+};
+
+post = function(url, args, xstate, callback, snap) {
+  var ar, fdata, k, v, xmlHttp;
+  xmlHttp = null;
+  if (window.XMLHttpRequest) {
+    xmlHttp = new XMLHttpRequest();
+  } else {
+    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+  }
+  xmlHttp.withCredentials = true;
+  ar = [];
+  for (k in args) {
+    v = args[k];
+    if (v && v !== "") {
+      ar.push(k + "=" + escape(v));
+    }
+  }
+  fdata = ar.join("&");
+  xmlHttp.open("POST", url, true);
+  xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+  xmlHttp.send(fdata);
+  return xmlHttp.onreadystatechange = function(state) {
+    var e, response;
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 500) {
+      if (snap) {
+        snap(xstate);
+      }
+    }
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
+      if (callback) {
+        try {
+          response = JSON.parse(xmlHttp.responseText);
+          return callback(response, xstate);
+        } catch (_error) {
+          e = _error;
+          return callback(JSON.parse(xmlHttp.responseText), xstate);
+        }
+      }
+    }
+  };
+};
+
+postJSON = function(url, json, xstate, callback, snap) {
+  var fdata, xmlHttp;
+  xmlHttp = null;
+  if (window.XMLHttpRequest) {
+    xmlHttp = new XMLHttpRequest();
+  } else {
+    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+  }
+  xmlHttp.withCredentials = true;
+  fdata = JSON.stringify(json);
+  xmlHttp.open("POST", url, true);
+  xmlHttp.setRequestHeader("Content-type", "application/json");
+  xmlHttp.send(fdata);
+  return xmlHttp.onreadystatechange = function(state) {
+    var e, response;
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 500) {
+      if (snap) {
+        snap(xstate);
+      }
+    }
+    if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
+      if (callback) {
+        try {
+          response = JSON.parse(xmlHttp.responseText);
+          if (response && response.loginRequired) {
+            location.href = "/oauth.html";
+            return;
+          }
+          return callback(response, xstate);
+        } catch (_error) {
+          e = _error;
+          return callback(JSON.parse(xmlHttp.responseText), xstate);
+        }
+      }
+    }
+  };
+};
+
+isArray = function(value) {
+  return value && typeof value === 'object' && value instanceof Array && typeof value.length === 'number' && typeof value.splice === 'function' && !(value.propertyIsEnumerable('length'));
+};
+
+
+/* isHash: function to detect if an object is a hash */
+
+isHash = function(value) {
+  return value && typeof value === 'object' && !isArray(value);
+};
+
+tabs = [];
+
+jsdata = {};
+
+mergeData = function(json, pmc) {
+  var i, j, key, l, len1, len2, ref, todo;
+  if (!pmc) {
+    jsdata = json;
+    return;
+  }
+  if (indexOf.call(jsdata.pmcs, pmc) >= 0) {
+    return;
+  }
+  if (nproject && nproject.length > 0) {
+    ref = jsdata.pmcs;
+    for (j = 0, len1 = ref.length; j < len1; j++) {
+      i = ref[j];
+      if (jsdata.pmcs[i] === nproject) {
+        jsdata.pmcs.splice(i, 1);
+        break;
+      }
+    }
+  }
+  todo = new Array('count', 'mail', 'delivery', 'bugzilla', 'jira', 'changes', 'pmcdates', 'pdata', 'releases', 'keys', 'health');
+  for (l = 0, len2 = todo.length; l < len2; l++) {
+    i = todo[l];
+    key = todo[i];
+    jsdata[key][pmc] = json[key][pmc];
+  }
+  jsdata.pmcs.push(pmc);
+  return nproject = pmc;
+};
+
+preloadTabs = function(json, state) {
+  var a, cpmc, ctab, j, len1, pmc, ref, tab;
+  cpmc = isHash(state) && state.pmc ? state.pmc : null;
+  mergeData(json, cpmc);
+  tabs = [];
+  a = 0;
+  ctab = 0;
+  ref = jsdata.pmcs;
+  for (j = 0, len1 = ref.length; j < len1; j++) {
+    pmc = ref[j];
+    tab = {
+      id: pmc,
+      title: pmc,
+      renderer: renderFrontPage,
+      state: pmc
+    };
+    tabs.push(tab);
+    if (cpmc === pmc) {
+      ctab = a;
+    }
+    a++;
+  }
+  return loadTabs(ctab);
+};
+
+currentTab = 0;
+
+loadTabs = function(stab) {
+  var all, bread, j, k, l, len1, len2, main, pmc, ref, sel, tab, tdiv, title, v;
+  main = new HTML('div', {
+    id: 'dialog'
+  });
+  document.getElementById('wrapper').innerHTML = "";
+  document.getElementById('wrapper').appendChild(main);
+  tdiv = new HTML('div', {
+    "class": 'tabs'
+  });
+  main.inject(tdiv);
+  k = 0;
+  for (j = 0, len1 = tabs.length; j < len1; j++) {
+    v = tabs[j];
+    if ((stab && stab === k) || (!stab && k === 0)) {
+      currentTab = k;
+      history.pushState(null, null, "?" + k);
+      tab = new HTML('div', {
+        "class": 'tablink tablink_selected'
+      }, v.title);
+      title = new HTML('h2', {}, v.title + ":");
+      main.inject(title);
+    } else {
+      tab = new HTML('div', {
+        "class": 'tablink',
+        onclick: "loadTabs(" + k + ");"
+      }, v.title);
+    }
+    tdiv.inject(tab);
+    k++;
+  }
+  all = ['Add a tab:', '---------------'];
+  ref = jsdata.all || [];
+  for (l = 0, len2 = ref.length; l < len2; l++) {
+    pmc = ref[l];
+    all.push(pmc);
+  }
+  sel = makeSelect('project', all);
+  sel.setAttribute("onchange", "addTab(this.value);");
+  tdiv.inject(sel);
+  bread = new HTML('div', {
+    "class": 'bread',
+    id: 'contents'
+  });
+  main.inject(bread);
+  return loadBread(stab);
+};
+
+addTab = function(pmc) {
+  return fetch("getjson.py?only=" + pmc, {
+    pmc: pmc
+  }, preloadTabs);
+};
+
+loadBread = function(which) {
+  if (tabs[which] && tabs[which].renderer) {
+    document.getElementById('contents').innerHTML = "";
+    return tabs[which].renderer(tabs[which].state);
+  }
+};

Added: comdev/reporter.apache.org/trunk/site/ng.html
URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/ng.html?rev=1799040&view=auto
==============================================================================
--- comdev/reporter.apache.org/trunk/site/ng.html (added)
+++ comdev/reporter.apache.org/trunk/site/ng.html Sun Jun 18 06:59:01 2017
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8"/>
+<title>Apache Committee Report Helper</title>
+<link href="css/tabs.css" rel="stylesheet"/>
+</head>
+<body onload="loadTabs();">
+    <h1 style="color: #FFF; text-align: center;">Apache Committee Report Helper</h1>
+    <div id="wrapper" style="width: 100%; height: 700px; text-align: center;">
+        Loading, hang on...
+    </div>
+    <p style="text-align: center; color: #eee;">
+        <!-- insert footer here -->
+    </p>
+    <script type="text/javascript" src="js/tabs.js"></script>
+    <script src="js/vendor/modernizr.js"></script>
+   <script src="https://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
+   <script src="https://code.jquery.com/ui/1.11.3/jquery-ui.js" type="text/javascript"></script>
+   <link rel="stylesheet" href="//code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css">
+   <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+   <script type="text/javascript">
+   google.load("visualization", "1", {packages:["corechart", "timeline"]});
+   google.setOnLoadCallback(function() {
+      var project = document.location.search.substr(1);
+      fetch("getjson.py", project, preloadTabs);
+   });
+</script>
+</body>
+</html>