Merge branch 'dynamicwholist' of https://github.com/apache/cloudstack-www into 413update
diff --git a/source/javascripts/phonebook.js b/source/javascripts/phonebook.js
new file mode 100644
index 0000000..74a4b31
--- /dev/null
+++ b/source/javascripts/phonebook.js
@@ -0,0 +1,917 @@
+var pmcs = [] // array of PMC names (excludes non-PMC committees)
+var people = {} // public_ldap_people.json
+var ldapauth = {} // public_ldap_authgroups.json
+// TODO don't rely on ldap_groups containing PMC :members groups
+var ldapgroups = {} //  public_ldap_groups.json
+var ldapservices = {} // public_ldap_services.json
+var ldapprojects = {} // public_ldap_projects.json
+
+var members = {} // copy of member-info.json
+var committees = {} // copy of committee-info.json (plus details for 'member' dummy PMC)
+var iclainfo = {} // copy of icla-info.json (committers only)
+var podlings = {} // public_ldap_projects.json where podling is 'current'
+
+var info = {} // copies of json info
+
+// Constants for query types. 
+// Do NOT change the values once established, as they are part of the public API
+// For example they may be used in projects.a.o and reporter.a.o
+// The values are used for matching HTTP queries and linkifying lists (to generate a valid HTML link)
+
+var Q_USER = 'user' // search users
+var Q_PROJECT = 'project' // search PMC names
+var Q_UID = 'uid' // availid, exact match
+var Q_PMC = 'pmc' // PMC, exact match
+var Q_UNIX = 'unix' // LDAP group
+var Q_CTTE = 'ctte' // LDAP group
+var Q_SERVICE = 'service' // LDAP group
+var Q_PODLING = 'podling' // podling (non-LDAP group)
+var Q_AUTH = 'auth' // podling (LDAP auth group)
+
+
+// Not intended for general use; may change at any time
+var Q_DEBUG = 'debug' // print some debug info
+
+// compatibility shim for IE8 and other older browsers
+if (!Date.now) {
+   Date.now = function() {
+      return new Date().getTime();
+   }
+}
+
+// This is faster than parseInt, and it's more obvious why it is being done
+function toInt(number) {
+   return number | 0 //
+}
+
+var fetchCount = 0;
+// Fetch an array of URLs, each with their description and own callback plus a final callback
+// Used to fetch everything before rendering a page that relies on multiple JSON sources.
+
+
+function getAsyncJSONArray(urls, finalCallback) {
+   var obj = document.getElementById('progress');
+   if (fetchCount == 0) {
+      fetchCount = urls.length;
+   }
+
+   if (urls.length > 0) {
+      var a = urls.shift();
+      var URL = a[0];
+      var desc = a[1];
+      var cb = a[2];
+      var xmlHttp = null;
+      if (window.XMLHttpRequest) {
+         xmlHttp = new XMLHttpRequest();
+      } else {
+         xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+      }
+
+      if (obj) {
+         obj.innerHTML = "loading file #" + (fetchCount - urls.length) + " / " + fetchCount + "<br>" + desc
+      }
+      var start = Date.now()
+      xmlHttp.open("GET", URL, true);
+      xmlHttp.onreadystatechange = function(state) {
+         if (xmlHttp.readyState == 4) {
+            if (cb) {
+               if (xmlHttp.status == 200) {
+                  elapsed = Date.now() - start
+                  cb(JSON.parse(xmlHttp.responseText));
+                  // must be done after as cb creates the hash
+                  info[desc]['elapsed'] = elapsed
+               } else {
+                  cb({});
+                  alert("Error: '" + xmlHttp.statusText + "' while loading " + URL)
+               }
+            }
+            getAsyncJSONArray(urls, finalCallback);
+         }
+      }
+      xmlHttp.send(null);
+   } else {
+      if (obj) {
+         obj.innerHTML = "building page content..."
+      }
+      finalCallback();
+   }
+}
+
+// get list of projects on which uid is a committer
+function getProjectCommittership(uid) {
+   var cl = []
+   for (var i in ldapprojects) {
+      if (ldapprojects[i].pmc && ldapprojects[i].members.indexOf(uid) > -1) {
+         cl.push(i)
+      }
+   }
+   return cl
+}
+
+//get list of projects on which uid is an owner (member karma)
+function getProjectOwnership(uid) {
+   var cl = []
+   for (var i in ldapprojects) {
+      if (ldapprojects[i].pmc && ldapprojects[i].owners.indexOf(uid) > -1) {
+         cl.push(i)
+      }
+   }
+   return cl
+}
+
+// Get the roster from a json group
+// returns: all the keys where the uid is [not] a member
+function getRoster(json, uid, notIn) {
+   var cl = []
+   for (var i in json) {
+      if (json[i].roster.indexOf(uid) > -1) {
+         if (typeof notIn === 'undefined') {
+            cl.push(i)
+         } else {
+            if (notIn.indexOf(i) == -1) {
+               cl.push(i)
+            }
+         }
+      }
+   }
+   return cl
+}
+
+// get data from committee-info for a person
+// return [list of pmcs, list of chairs]
+function getCommitteeRoles(uid) {
+   var pl = []
+   var ch = []
+   for (var i in committees) {
+      // Only list actual PMCs
+      if (committees[i].pmc && uid in committees[i].roster) {
+         pl.push(i)
+      }
+      //var chair = committees[i].chair // might not be one (eg members)
+      //if (chair && uid in committees[i].chair) {
+         ch.push(i)
+      //}
+   }
+   return [pl, ch]
+}
+
+function getCommitterName(uid) {
+   var noicla = {
+      'andrei': '(Andrei Zmievski)',
+      'pcs': '(Paul Sutton)',
+      'rasmus': '(Rasmus Lerdorf)'
+   }
+   var name
+   if (uid in people) { // it's possible for a list to contain a uid that is not in people (e.g. andrei in member)
+      name = people[uid].name
+   }
+   if (!name) {
+      name = iclainfo[uid]
+   }
+   if (!name) { // try the backup specials
+      name = noicla[uid]
+   }
+   return name
+}
+
+// Linkify list of group names by adding the appropriate ?type= href
+
+function linkifyList(type, names) {
+   var text = ''
+   var index, len, i
+   names.sort()
+   for (i = 0, len = names.length; i < len; ++i) {
+      if (i > 0) {
+         text = text + ", "
+      }
+      text = text + "<a href='?" + type + "=" + names[i] + "'>" + names[i] + "</a>"
+   }
+   return text
+}
+
+// Linkify user ids
+
+function userList(ua) {
+   var text = ''
+   var index, len
+   ua.sort()
+   for (index = 0, len = ua.length; index < len; ++index) {
+      if (index > 0) {
+         text = text + ", "
+      }
+      text = text + hiliteMember(ua[index])
+   }
+   return text
+}
+
+//Linkify URLs
+
+function linkifyURLs(ua) {
+   var text = ''
+   var index, len
+   ua.sort()
+   for (index = 0, len = ua.length; index < len; ++index) {
+      if (index > 0) {
+         text = text + ", "
+      }
+      text = text + "<a target='_blank' href='" + ua[index] + "'>" + ua[index] + "</a>"
+   }
+   return text
+}
+
+function showCommitter(obj, uid) {
+   var details = document.getElementById('details_committer_' + uid)
+   if (!details) {
+      details = document.createElement('p')
+      details.setAttribute("id", 'details_committer_' + uid)
+      var cl = getProjectCommittership(uid) // committer(in :members) of these LDAP PMC projects 
+      var roles = getCommitteeRoles(uid)
+      var cttees = getProjectOwnership(uid) // member(in :owners) of these LDAP PMC projects
+      var pl = roles[0] // pmc membership
+      var ch = roles[1] // chairs
+      if (isNologin(uid)) {
+         details.innerHTML += "<b>Login is currently disabled</b><br/><br/>"
+      }
+      if (isMember(uid)) {
+         details.innerHTML += "<i>Foundation member</i><br/><br/>"
+      }
+      if (ch.length > 0) {
+         details.innerHTML += "<b>Chair of:</b> " + linkifyList(Q_PMC, ch)
+         if (!isChair(uid)) {
+            details.innerHTML += " <b>Not a member of pmc-chairs!</b>"
+         }
+         details.innerHTML += "<br/><br/>"
+      }
+      var purls = urls(uid)
+      if (purls.length > 0) {
+         details.innerHTML += "<b>Personal URLs:</b> " + linkifyURLs(purls) + "<br/><br/>"
+      }
+      if (cl.length > 0) {
+         details.innerHTML += "<b>Committer on:</b> " + linkifyList(Q_UNIX, cl) + "<br/><br/>"
+      }
+      var nc = [] // On PMC but not in LDAP unix
+      var nl = [] // On PMC but not in LDAP committee
+      var np = [] // Not in PMC even though in LDAP committee
+      var nu = [] // In LDAP committee but not in LDAP unix
+      var pn;
+      if (pl.length > 0) {
+         details.innerHTML += "<b>PMC member of:</b> " + linkifyList(Q_PMC, pl) + "<br/><br/>"
+         for (p in pl) {
+            pn = pl[p]
+            // There is an LDAP PMC group but the uid is not in the committer(:members) group
+            if (isProjectPMC(pn) && cl.indexOf(pn) < 0) {
+               nc.push(pn)
+            }
+            // There is an LDAP PMC group but the uid is not in the committee(:owners) group
+            if (isProjectPMC(pn) && cttees.indexOf(pn) < 0) {
+               nl.push(pn)
+            }
+         }
+      }
+
+      if (cttees.length > 0) {
+         for (p in cttees) {
+            pn = cttees[p]
+            // name is a PMC but uid is not on the PMC
+            if (isPMC(pn) && pl.indexOf(pn) < 0) {
+               np.push(pn)
+            }
+            // name has LDAP project entry but uid is not in the committer (member) list  
+            if (isProjectPMC(pn) && cl.indexOf(pn) < 0) {
+               nu.push(pn)
+            }
+         }
+         details.innerHTML += "<b>LDAP committee group membership:</b> " + linkifyList(Q_CTTE, cttees) + "<br/><br/>"
+      }
+
+      var services = getRoster(ldapservices, uid)
+      if (services.length > 0) {
+         details.innerHTML += "<b>Service group membership:</b> " + linkifyList(Q_SERVICE, services) + "<br/><br/>"
+      }
+      var auths = getRoster(ldapauth, uid)
+      if (auths.length > 0) {
+         details.innerHTML += "<b>Auth group membership:</b> " + linkifyList(Q_AUTH, auths) + "<br/><br/>"
+      }
+      var pods = getRoster(podlings, uid)
+      if (pods.length > 0) {
+         details.innerHTML += "<b>Podling membership:</b> " + linkifyList(Q_PODLING, pods) + "<br/><br/>"
+      }
+
+      // Note any discrepancies
+      if (np.length > 0) {
+         details.innerHTML += "<span class='error'>In LDAP committee group, but <b>not a PMC member</b>:</span> " + linkifyList(Q_CTTE, np) + "<br/><br/>"
+      }
+      if (nc.length > 0) {
+         details.innerHTML += "<span class='error'>On PMC, but not a member of the committer group:</span> " + linkifyList(Q_PMC, nc) + "<br/><br/>"
+      }
+      if (nl.length > 0) {
+         details.innerHTML += "<span class='error'>On PMC, but not member of the LDAP committee group:</span> " + linkifyList(Q_CTTE, nl) + "<br/><br/>"
+      }
+      if (nu.length > 0) {
+         details.innerHTML += "<span class='error'>In LDAP committee group but not a member of the committer group:</span> " + linkifyList(Q_UNIX, nu) + "<br/><br/>"
+      }
+      obj.appendChild(details)
+   } else {
+      obj.removeChild(details)
+   }
+}
+
+function hoverCommitter(parent, uid) {
+   var div = document.getElementById('hoverbar')
+
+   // If the datepicker object doesn't exist, spawn it
+   if (!div) {
+      div = document.createElement('div')
+      document.body.appendChild(div)
+      div.setAttribute("id", "hoverbar")
+      div.style.position = "fixed"
+      div.style.width = "400px"
+      div.style.background = "linear-gradient(to bottom, rgba(254,255,232,1) 0%,rgba(214,219,191,1) 100%)"
+      div.style.borderRadius = "4px"
+      div.style.border = "1px solid #333"
+      div.style.zIndex = "9999"
+   }
+
+   // Reset the contents of the datepicker object
+   div.innerHTML = ""
+
+   var bb = parent.getBoundingClientRect()
+   div.style.top = (bb.bottom + 24) + "px"
+   div.style.left = (bb.left + 32) + "px"
+
+   if (uid) {
+      div.style.display = "block"
+      div.innerHTML = "<h4>" + getCommitterName(uid) + "</h4>"
+      var cl = getProjectCommittership(uid)
+      var roles = getCommitteeRoles(uid)
+      var pl = roles[0]
+      var ch = roles[1]
+      if (isMember(uid) == true) {
+         div.innerHTML += "<i>Foundation member</i><br/><br/>"
+      }
+      if (isNologin(uid)) {
+         div.innerHTML += "<b>Login is currently disabled</b><br/><br/>"
+      }
+      if (ch.length > 0) {
+         ch.sort()
+         div.innerHTML += "<b>Chair of:</b> " + ch.join(", ")
+         if (!isChair(uid)) {
+            div.innerHTML += " <b>Not a member of pmc-chairs!</b>"
+         }
+         div.innerHTML += "<br/><br/>"
+      }
+      if (cl.length > 0) {
+         cl.sort()
+         div.innerHTML += "<b>Committer on:</b> " + cl.join(", ") + "<br/><br/>"
+      }
+      var nc = []
+      if (pl.length > 0) {
+         pl.sort()
+         div.innerHTML += "<b>PMC member of:</b> " + pl.join(", ") + "<br/><br/>"
+         for (p in pl) {
+            var pn = pl[p]
+            if (pn != 'member' && cl.indexOf(pn) < 0) {
+               nc.push(pn)
+            }
+         }
+      }
+      if (nc.length > 0) {
+         div.innerHTML += "<i>On PMC, but not a Committer on:</i> " + nc.join(", ") + "<br/><br/>"
+      }
+
+
+   } else {
+      div.style.display = "none"
+   }
+}
+
+function isNologin(uid) {
+   return !(uid in people) || people[uid].noLogin
+}
+
+function isMember(uid) {
+   return members['members'].indexOf(uid) > -1
+}
+
+function isChair(uid) {
+   return ldapservices['pmc-chairs'].roster.indexOf(uid) > -1
+}
+
+function urls(uid) {
+   return people[uid].urls || []
+}
+
+// Is a PMC according to committee-info
+function isPMC(name) {
+   return pmcs.indexOf(name) >= 0;
+}
+
+//Is a PMC according to ldap_projects
+function isProjectPMC(name) {
+   return name in ldapprojects && ldapprojects[name].pmc
+}
+
+function linkifyUid(uid) {
+   if (isNologin(uid)) {
+      return uid
+   }
+   return uid
+}
+
+function hiliteMember(uid) {
+   if (isMember(uid)) {
+      return linkifyUid(uid)
+   } else {
+      return linkifyUid(uid)
+   }
+}
+
+function getChair(uid) {
+   var chair = committees[uid].chair
+   if (chair) {
+      for (var x in chair) {
+         return chair[x].name
+      }
+   }
+   return null
+}
+
+function showProject(obj, prj) {
+   var details = document.getElementById('details_project_' + prj)
+   if (!details) {
+      details = document.createElement('p')
+      details.setAttribute("id", 'details_project_' + prj)
+      var desc = committees[prj].description
+      if (!desc) {
+         desc = 'TBA (please ensure that <a href="http://www.apache.org/index.html#projects-list">the projects list</a> is updated)'
+      }
+      var chair = getChair(prj)
+      if (chair) {
+         details.innerHTML += "<b>Chair:</b> " + chair + "<br/><br/>"
+      }
+      var url = committees[prj].site
+      var cl
+      var clExists = false // Does the unix group exist?
+      try {
+         cl = ldapgroups[prj].roster.slice()
+         clExists = true
+      } catch (err) { // Allow for missing Unix group
+         cl = []
+      }
+      var pl = []
+      var pmc = committees[prj]
+
+      var pmcnoctte = [] // on pmc but not in LDAP committee
+      var cttenopmc = [] // In LDAP ctte but not on PMC
+      var ldappmc = []
+      var ctteeExists = false
+      if (isProjectPMC(prj)) { // may not exist, e.g. for 'member' PMC and if group has yet to be created
+         ldappmc = ldapprojects[prj].owners
+         ctteeExists = true
+      }
+      var pmcnounix = [] // on PMC but not in LDAP unix group
+      var cttenounix = [] // In LDAP ctte but not in LDAP unix
+      if (pmc) {
+         for (var c in pmc.roster) {
+            pl.push(c)
+         }
+         for (var i in ldappmc) {
+            if (!(ldappmc[i] in pmc.roster)) {
+               cttenopmc.push(ldappmc[i])
+            }
+         }
+      }
+      cl.sort()
+      pl.sort()
+
+      // Must use cl before it is re-used to hold the entries
+      if (clExists && prj != 'member') { // does not exist for 'member' PMC
+         for (var i in ldappmc) {
+            var id = ldappmc[i]
+            if (cl.indexOf(id) < 0) { // in LDAP cttee but not in LDAP unix
+               cttenounix.push(id)
+            }
+         }
+      }
+
+      for (var i in pl) {
+         var id = pl[i]
+         pl[i] = "<tr><td><b>" + getCommitterName(pl[i]) + "</b></td><td>(" + hiliteMember(pl[i]) + ")</td></tr>"
+         if (clExists && cl.indexOf(id) < 0) { // On PMC but not in LDAP unix group
+            pmcnounix.push(id)
+         }
+         if (prj != 'member' && ldappmc && ldappmc.indexOf(id) < 0) { // in PMC but not in LDAP committee (does not apply to member)
+            pmcnoctte.push(id)
+         }
+      }
+
+      for (var i in cl) {
+         cl[i] = "<tr><td><b>" + getCommitterName(cl[i]) + "</b></td><td>(" + hiliteMember(cl[i]) + ")</td></tr>"
+      }
+
+      if (pl.length > 0) {
+         if (prj == 'member') {
+            details.innerHTML += "<b>ASF members</b><br><br><table>" + pl.join("\n") + "</table><br/>"
+         } else {
+            details.innerHTML += "<h4>PMC members (also in the committer group): " + pl.length + "</h4><table>" + pl.join("\n") + "</table><br/>"
+         }
+      }
+
+      if (cl && cl.length > 0) {
+         details.innerHTML += "<h4>Committers: " + cl.length + "</h4><table>" + cl.join("\n") + "</table>"
+         if (podlings[prj]) {
+            details.innerHTML += "<span class='error'>WARNING: <a href='?podling=" + prj + "'>" + prj + " podling group</a> also exists - this can cause authentication issues</span><br/><br/>"
+         }
+      } else {
+         if (!clExists) {
+            details.innerHTML += "<span class='error'>LDAP unix group not present!</span><br/><br/>"
+         }
+      }
+
+      if (pmcnoctte.length) {
+         if (ctteeExists) {
+            details.innerHTML += "<span class='error'>PMC members not in LDAP committee group:</span> " + userList(pmcnoctte) + "<br/><br/>"
+         } else {
+            details.innerHTML += "<span class='error'>LDAP committee group not present!</span><br/><br/>"
+         }
+      }
+      if (pmcnounix.length) {
+         if (prj == 'member') {
+            details.innerHTML += "<span class='error'>ASF members not in committers(unix) group:</span> " + userList(pmcnounix) + "<br/><br/>"
+         } else {
+            details.innerHTML += "<span class='error'>PMC members not in committers(unix) group:</span> " + userList(pmcnounix) + "<br/><br/>"
+         }
+      }
+      if (cttenounix.length) {
+         details.innerHTML += "<span class='error'>LDAP committee group members not in committers(unix) group:</span> " + userList(cttenounix) + "<br/><br/>"
+      }
+      if (cttenopmc.length) {
+         details.innerHTML += "<span class='error'>LDAP committee group members not on PMC:</span> " + userList(cttenopmc) + "<br/><br/>"
+      }
+
+
+      obj.appendChild(details)
+   } else {
+      obj.removeChild(details)
+   }
+}
+
+// Generic group display function
+// attr is either missing or 'owners' or 'members'
+function showJsonRoster(obj, type, json, name, attr, checkUnix) {
+   var id = 'details_' + type + '_' + name
+   var details = document.getElementById(id)
+   if (!details) {
+      details = document.createElement('p')
+      details.setAttribute("id", id)
+      var podtype = json[name]['podling']
+      if (podtype) {
+         details.innerHTML += "<b>podling:</b> " + podtype + "<br><br>"
+      }
+      var cl;
+      if (attr == 'owners') {
+         cl = json[name].owners.slice()
+      } else if (attr == 'members') {
+         cl = json[name].members.slice()
+      } else {
+         cl = json[name].roster.slice()
+      }
+      cl.sort()
+      for (var i in cl) {
+         var uid = cl[i]
+         cl[i] = "<tr><td onmouseover='hoverCommitter(this, \"" + uid + "\");' onmouseout='hoverCommitter(this, null);'><kbd>" + hiliteMember(uid) + "</kbd></td><td>" + getCommitterName(uid) + "</td>"
+         if (checkUnix) { // check against Unix group
+            if (ldapgroups[name]) { // make sure group exists!
+               if (ldapgroups[name].roster.indexOf(uid) > -1) {
+                  cl[i] += "<td>&nbsp;</td>"
+               } else {
+                  cl[i] += "<td> N.B. not found in corresponding Unix group</td>"
+               }
+            }
+         }
+         cl[i] += "</tr>"
+      }
+
+      if (cl && cl.length > 0) {
+         details.innerHTML += "<b>Roster:</b><br><br><table>" + cl.join("\n") + "</table><br/>"
+      }
+      obj.appendChild(details)
+   } else {
+      obj.removeChild(details)
+   }
+}
+
+// Show a single Service group
+function showServiceRoster(obj, name) {
+   showJsonRoster(obj, 'service', ldapservices, name)
+}
+
+// Show a single Auth group
+function showAuthRoster(obj, name) {
+   showJsonRoster(obj, 'auth', ldapauth, name)
+}
+
+function showPodlingRoster(obj, name) {
+   showJsonRoster(obj, 'podling', podlings, name)
+}
+
+// Show an LDAP Unix group
+
+function showGroup(obj, name) {
+   showJsonRoster(obj, 'group', ldapgroups, name)
+}
+
+// Show an LDAP Commiteee group
+
+function showCommittee(obj, name) {
+   showJsonRoster(obj, 'ctte', ldapprojects, name, 'owners', true)
+}
+
+function searchProjects(keyword, open) {
+   var obj = document.getElementById('phonebook')
+   if (keyword != '') {
+      obj.innerHTML = ""
+   } else {
+      obj.innerHTML = ''
+   }
+   for (var i in pmcs) {
+      var pmc = pmcs[i]
+      if (pmc.search(keyword.toLowerCase()) != -1) {
+         var ppmc = committees[pmc].display_name
+         obj.innerHTML += "<div id='project_" + pmc + "' class='group'><h3>Apache " + ppmc + "</h3></div>"
+         if (open) {
+            showProject(document.getElementById('project_' + pmc), pmc)
+         }
+      }
+   }
+}
+
+function searchService(keyword, open) {
+   var obj = document.getElementById('phonebook')
+   if (keyword != '') {
+      obj.innerHTML = "<h3>Search results:</h3><hr/>"
+   } else {
+      obj.innerHTML = ''
+   }
+   for (var srv in ldapservices) {
+      if (srv.search(keyword.toLowerCase()) != -1) {
+         obj.innerHTML += "<div id='service_" + srv + "' class='group'><h3 onclick=\"showServiceRoster(this.parentNode, '" + srv + "');\">" + srv + "</h3></div>"
+         if (open) {
+            showServiceRoster(document.getElementById('service_' + srv), srv)
+         }
+      }
+   }
+}
+
+function searchAuth(keyword, open) {
+   var obj = document.getElementById('phonebook')
+   if (keyword != '') {
+      obj.innerHTML = "<h3>Search results:</h3><hr/>"
+   } else {
+      obj.innerHTML = ''
+   }
+   for (var auth in ldapauth) {
+      if (auth.search(keyword.toLowerCase()) != -1) {
+         obj.innerHTML += "<div id='auth_" + auth + "' class='group'><h3 onclick=\"showAuthRoster(this.parentNode, '" + auth + "');\">" + auth + "</h3></div>"
+         if (open) {
+            showauthRoster(document.getElementById('auth_' + auth), auth)
+         }
+      }
+   }
+}
+
+// Show a single PMC
+
+function showPMC(pmc) {
+   var obj = document.getElementById('phonebook')
+   if (pmc in committees) {
+      var ppmc = committees[pmc].display_name
+      obj.innerHTML = "<div id='project_" + pmc + "' class='group'><h3 onclick=\"showProject(this.parentNode, '" + pmc + "');\">Apache " + ppmc + "</h3></div>"
+      showProject(document.getElementById('project_' + pmc), pmc)
+   } else {
+      obj.innerHTML = "<h3>Could not find PMC: '" + pmc + "'</h3>"
+   }
+}
+
+// Show a single Unix Group
+
+function showUNIX(unix) {
+   var obj = document.getElementById('phonebook')
+   var id = 'group_' + unix
+   if (unix in ldapgroups) {
+      obj.innerHTML = "<div id='" + id + "' class='group'><h3 onclick=\"showGroup(this.parentNode, '" + unix + "');\">" + unix + " (LDAP unix group)</h3></div>"
+      showGroup(document.getElementById(id), unix)
+   } else {
+      obj.innerHTML = "<h3>Could not find unix group: '" + unix + "'</h3>"
+   }
+}
+
+// Show a single Committee group
+
+function showCTTE(ctte) {
+   var obj = document.getElementById('phonebook')
+   var id = 'ctte_' + ctte
+   if (ctte in ldapprojects && ldapprojects[ctte].pmc) {
+      obj.innerHTML = "<div id='" + id + "' class='group'><h3 onclick=\"showCommittee(this.parentNode, '" + ctte + "');\">" + ctte + " (LDAP committee group)</h3></div>"
+      showCommittee(document.getElementById(id), ctte)
+   } else {
+      obj.innerHTML = "<h3>Could not find committee group: '" + ctte + "'</h3>"
+   }
+}
+
+function showSVC(name) {
+   var obj = document.getElementById('phonebook')
+   var id = 'service_' + name
+   if (name in ldapservices) {
+      obj.innerHTML = "<div id='" + id + "' class='group'><h3 onclick=\"showServiceRoster(this.parentNode, '" + name + "');\">" + name + " (LDAP service group)</h3></div>"
+      showServiceRoster(document.getElementById(id), name)
+   } else {
+      obj.innerHTML = "<h3>Could not find the service group: '" + name + "'</h3>"
+   }
+}
+
+function showAUTH(name) {
+   var obj = document.getElementById('phonebook')
+   var id = 'auth_' + name
+   if (name in ldapauth) {
+      obj.innerHTML = "<div id='" + id + "' class='group'><h3 onclick=\"showAuthRoster(this.parentNode, '" + name + "');\">" + name + " (LDAP auth group)</h3></div>"
+      showAuthRoster(document.getElementById(id), name)
+   } else {
+      obj.innerHTML = "<h3>Could not find the auth group: '" + name + "'</h3>"
+   }
+}
+
+function showPOD(name) {
+   var obj = document.getElementById('phonebook')
+   var id = 'podling_' + name
+   if (name in podlings) {
+      obj.innerHTML = "<div id='" + id + "' class='group'><h3 onclick=\"showPodlingRoster(this.parentNode, '" + name + "');\">" + name + " (podling)</h3></div>"
+      showPodlingRoster(document.getElementById(id), name)
+   } else {
+      obj.innerHTML = "<h3>Could not find the podling: '" + name + "'</h3>"
+   }
+}
+
+function searchPodlings(keyword, open) {
+   var obj = document.getElementById('phonebook')
+   obj.innerHTML = "<h3>Search results:</h3><hr/>"
+   for (var name in podlings) {
+      if (name.search(keyword.toLowerCase()) != -1) {
+         var id = 'podling_' + name
+         obj.innerHTML += "<div id='" + id + "' class='group'><h3 onclick=\"showPodlingRoster(this.parentNode, '" + name + "');\">" + name + " (podling)</h3></div>"
+      }
+   }
+}
+
+function showDBG(name) {
+   var obj = document.getElementById('phonebook')
+   if (name == 'info') {
+      obj.innerHTML = "<h3>info</h3>"
+      obj.innerHTML += "<pre>" + JSON.stringify(info, null, 1) + "</pre>"
+
+   } else {
+      obj.innerHTML = "<h3>Unknown debug name: '" + name + "'</h3>"
+   }
+}
+
+// Show a single User
+
+function showUid(uid) {
+   var obj = document.getElementById('phonebook')
+   if (uid in people) {
+      var name = getCommitterName(uid)
+      obj.innerHTML = "<div class='group' id='committer_" + uid + "'><h4 onclick=\"showCommitter(this.parentNode, '" + uid + "');\">" + name + " (<kbd>" + uid + "</kbd>)</h4></div>"
+      showCommitter(document.getElementById('committer_' + uid), uid)
+   } else {
+      obj.innerHTML = "<h3>Could not find user id: '" + uid + "'</h3>"
+   }
+}
+
+function showError(error) {
+   var obj = document.getElementById('phonebook')
+   if (typeof(error) === 'string') {
+      obj.innerHTML = "<h3>Error detected</h3>"
+      obj.innerHTML += error
+   } else { // assume it's an error object
+      obj.innerHTML = "<h3>Javascript Error detected</h3>"
+      obj.innerHTML += "<hr/>"
+      obj.innerHTML += "<pre>" + error.message + "</pre>"
+      obj.innerHTML += "<pre>" + error.stack + "</pre>"
+      obj.innerHTML += "<hr/>"
+   }
+}
+
+function searchCommitters(keyword, open) {
+   if (keyword.length < 2) {
+      return
+   }
+   var n = 0
+   var obj = document.getElementById('phonebook')
+   obj.innerHTML = ""
+   for (var uid in people) {
+      if (!people[uid].noLogin) { // don't display disabled logins
+         var name = getCommitterName(uid)
+         if (uid.search(keyword.toLowerCase()) != -1 || name.toLowerCase().search(keyword.toLowerCase()) != -1) {
+            n++
+            if (n > 50) {
+               return;
+            }
+            obj.innerHTML += "<div class='group' id='committer_" + uid + "'><h4 onclick=\"showCommitter(this.parentNode, '" + uid + "');\">" + name +
+               " (<kbd>" + uid + "</kbd>) <a title='Link to committer details' href='phonebook.html?uid=" + uid + "'>&#149</a></h4></div>"
+            if (open) {
+               showCommitter(document.getElementById('committer_' + uid), uid)
+            }
+         }
+      }
+   }
+}
+
+function saveInfo(json, name) {
+   info[name] = {}
+   try {
+      info[name]['lastTimestamp'] = json.lastTimestamp
+   } catch (err) {} // ignored
+   try {
+      info[name]['lastCreateTimestamp'] = json.lastCreateTimestamp
+   } catch (err) {} // ignored
+   try {
+      info[name]['last_updated'] = json.last_updated
+   } catch (err) {} // ignored
+}
+
+function preRender() {
+   getAsyncJSONArray([
+         ['https://whimsy.apache.org/public/public_ldap_projects.json', "projects", function(json) {
+            ldapprojects = json.projects;
+            saveInfo(json, 'projects');
+         }],
+         ['https://whimsy.apache.org/public/member-info.json', "members", function(json) {
+            members = json;
+            saveInfo(json, 'members');
+         }],
+         ['https://whimsy.apache.org/public/public_ldap_people.json', "people", function(json) {
+            people = json.people;
+            saveInfo(json, 'people');
+         }],
+         ['https://whimsy.apache.org/public/committee-info.json', "committees", function(json) {
+            committees = json.committees;
+            saveInfo(json, 'committees');
+         }],
+         ['https://whimsy.apache.org/public/icla-info.json', "iclainfo", function(json) {
+            iclainfo = json.committers;
+            saveInfo(json, 'iclainfo');
+         }],
+         ['https://whimsy.apache.org/public/public_ldap_groups.json', "ldapgroups", function(json) {
+            ldapgroups = json.groups;
+            saveInfo(json, 'ldapgroups');
+         }],
+         ['https://whimsy.apache.org/public/public_ldap_authgroups.json', "ldapauth", function(json) {
+            ldapauth = json.auth;
+            saveInfo(json, 'ldapauth');
+         }],
+         ['https://whimsy.apache.org/public/public_ldap_services.json', "services", function(json) {
+            ldapservices = json.services;
+            saveInfo(json, 'services');
+         }],
+      ],
+      allDone);
+}
+
+// Called when all the async GETs have been completed
+
+function allDone() {
+   try {
+      pmcs = []
+      for (var k in committees) { // actual committees, not LDAP committee groups
+         if (committees[k].pmc) { // skip non-PMCs
+            pmcs.push(k)
+         }
+      }
+      for (var g in ldapprojects) {
+         // get podlings from projects
+         if (ldapprojects[g]['podling'] == 'current') {
+            podlings[g] = {}
+            podlings[g].roster = ldapprojects[g].members
+         }
+      }
+      pmcs.push('member')
+      pmcs.sort()
+      var mMap = {}
+      for (var m in members.members) {
+         mMap[members.members[m]] = {}
+      }
+      // copy across the members info
+      committees['member'] = {
+         'roster': mMap,
+         'display_name': 'Foundation Members',
+         'description': "Current ASF members (Committers == those with member karma)",
+         'site': 'http://www.apache.org/foundation/'
+      }
+
+      // Match ?type=name
+      searchProjects('cloudstack', true)
+
+   } catch (error) {
+      showError(error)
+   }
+}
\ No newline at end of file
diff --git a/source/who.html.markdown b/source/who.html.markdown
index bc10c5e..e44b827 100644
--- a/source/who.html.markdown
+++ b/source/who.html.markdown
@@ -15,208 +15,20 @@
 </div>
 
 </div>
+   <div id="phonebook">
 
-This page includes the Apache CloudStack Project Management Committee (PMC) members and committers.
-
-## PMC
-
-Active Project Management Committee contains (in alphabetical order of their usernames):
-
-{:.table-bordered}
-
-
-| Username | Name |
-|----------|------|
-|akarasulu|Alex Karasulu|
-|alena1108|Alena Prokharchyk|
-|animesh|Animesh|
-|andrijapanic|Andrija Panic|
-|boris|Boris Schrijver|
-|bstoyanov|Boris Stoyanov|
-|chipchilders|Chip Childers|
-|chiradeep|Chiradeep Vittal|
-|dahn|Daan|
-|ekho|Wilder Rodrigues|
-|erikw|Erik Weber|
-|giles|Giles Sirett|
-|gochiba|Go Chiba|
-|gabriel|Gabriel Beims Bräscher|
-|hogstrom|Matt Richard Hogstrom|
-|hugo|Hugo Trippaers|
-|ilya|Ilya Musayev|
-|jburwell|John Burwell|
-|jlk|John Kinsella|
-|jzb|Joe Brockmeier|
-|karenv|Karen Vuong|
-|ke4qqq|David Nalley|
-|kluge|Kevin Kluge|
-|milamber|Bruno Demion|
-|mlsorensen|Marcus Sorensen|
-|mnour|Mohammad Nour El-Din|
-|mrhinkle|Mark R. Hinkle|
-|mtutkowski|Mike Tutkowski|
-|nslater|Naomi Slater|
-|nux|Nux|
-|nathanejohnson|Nathan Johnson|
-|paul_a|Paul Angus|
-|pdion891|Pierre-Luc Dion|
-|rafael|Rafael Weingärtner|
-|rajani|Rajani Karuturi|
-|remi|Remi Bergsma|
-|resmo|Rene Moser|
-|rohit|Rohit Yadav|
-|sebgoa|Sebastien Goasguen|
-|sweller|Simon Weller|
-|swill|Will Stevens|
-|syed|Syed Ahmed|
-|svogel|Sven Vogel|
-|tsp|Prasanna|
-|weizhou|Wei Zhou|
-|widodh|Wido den Hollander|
-|willchan|William Chan|
-  
-  
-
-## Emeritus PMC Members
-
-PMC members who are no longer active include:
-
-+ Alex Huang (ahuang)
-+ Disheng Su (edison)
-+ Ian Duffy (duffy)
-+ Olivier Lamy (olamy)
-  
-  
-  
-## Committers
-
-Active list of committers (in alphabetical order of their usernames):
-
-{:.table-bordered}
-
-
-| Username | Name |
-|----------|------|
-|ahmad|Ahmad Emneina|
-|akarasulu|Alex Karasulu|
-|alena1108|Alena Prokharchyk|
-|amoghvk|Amogh Vasekar|
-|andrijapanic|Andrija Panic|
-|animesh|Animesh|
-|anthonyxu|Anthony Xu|
-|aprateek|Abhinandan Prateek|
-|bfederle|Brian Federle|
-|boris|Boris Schrijver|
-|brett|Brett Porter|
-|bstoyanov|Boris Stoyanov|
-|chipchilders|Chip Childers|
-|chiradeep|Chiradeep Vittal|
-|claytonweise|Clayton Weise|
-|csuich2|Chris Suich|
-|dahn|Daan|
-|darren|Darren Shepherd|
-|dcahill|Dave Cahill|
-|demetriust|Demetrius Tsitrelis|
-|devdeep|Devdeep Singh|
-|dkonrad|Dennis Konrad|
-|dsonstebo|Dag Sonstebo|
-|ekho|Wilder Rodrigues|
-|erikw|Erik Weber|
-|fmaximus|Frank Maximus|
-|frankzhang|Xin Zhang|
-|gabriel|Gabriel Beims Bräscher|
-|gaurav|Gaurav Nandkumar Aradhye|
-|gavinlee|Gavin Lee|
-|giles|Giles Sirett|
-|girish|Girish Prabhakar Shilamkar|
-|gochiba|Go Chiba|
-|haeena|Toshiaki Hatano|
-|harikrishna|Harikrishna Patnala|
-|higster|Geoff Higginbottom|
-|hogstrom|Matt Richard Hogstrom|
-|hugo|Hugo Trippaers|
-|ilya|Ilya Musayev|
-|isaacchiang|Isaac Chiang|
-|jayapal|Jayapal|
-|jbausewein|Jason Bausewein|
-|jburwell|John Burwell|
-|jessicawang|Jessica Wang|
-|jim|Jim Jagielski|
-|jlk|John Kinsella|
-|jtomechak|Jessica Tomechak|
-|jzb|Joe Brockmeier|
-|karenv|Karen Vuong|
-|kawai|Hiroaki Kawai|
-|kdamage|Kelcey Damage|
-|ke4qqq|David Nalley|
-|kelveny|Kelven Yang|
-|kirk|Kirk Kosinski|
-|kishan|Kishan|
-|kluge|Kevin Kluge|
-|kocka|Laszlo Hornyak|
-|koushik|Koushik Das|
-|likithas|Likitha Shetty|
-|marcaurele|Marc-Aurèle Brothier|
-|mchen|Min Chen|
-|mice|Mice Xia|
-|milamber|Bruno Demion|
-|mlsorensen|Marcus Sorensen|
-|mnour|Mohammad Nour El-Din|
-|mrhinkle|Mark R. Hinkle|
-|msinhore|Marco Sinhoreli|
-|mtutkowski|Mike Tutkowski|
-|muralireddy|Murali Mohan Reddy|
-|nathanejohnson|Nathan Johnson|
-|nitin|Nitin|
-|noa|Noa Resare|
-|nslater|Naomi Slater|
-|nux|Nux|
-|nvazquez|Nicolás Vázquez|
-|olgasmola|Olga Smola|
-|paul_a|Paul Angus|
-|pdion891|Pierre-Luc Dion|
-|pnguyen|Phong Nguyen|
-|prachidamle|Prachi Damle|
-|pranavs|Pranav Saxena|
-|pyr|Pierre-Yves Ritschard|
-|radhika|Radhika Nair|
-|rafael|Rafael Weingärtner|
-|rajani|Rajani Karuturi|
-|rajeshbattala|Rajesh Battala|
-|remi|Remi Bergsma|
-|resmo|Rene Moser|
-|rohit|Rohit Yadav|
-|sailajamada|Sailaja Mada|
-|saksham|Saksham Srivastava|
-|sangeethah|Sangeetha Hariharan|
-|sanjaytripathi|Sanjay Tripathi|
-|sanjeev|Sanjeev Neelarapu|
-|santhoshedukulla|Santhosh|
-|sateesh|Sateesh Chodapuneedi|
-|schhen|Sonny Heng Chhen|
-|sebgoa|Sebastien Goasguen|
-|serg38|Sergey Levitskiy|
-|slriv|Sam Robertson|
-|snuf|Funs Kessen|
-|sowmya|Sowmya Krishnan|
-|sudhap|Sudhap|
-|svogel|Sven Vogel|
-|swamy|Venkata Swamy|
-|sweller|Simon Weller|
-|swill|Will Stevens|
-|syed|Syed Ahmed|
-|talluri|Srikanteswararao Talluri|
-|tsp|Prasanna|
-|tuna|Anh Tu Nguyen|
-|vijayendrabvs|Vijayendra Bhamidipati|
-|weizhou|Wei Zhou|
-|widodh|Wido den Hollander|
-|willchan|William Chan|
-|yasker|Sheng Yang|
-|ynojima|Yoshikazu Nojima|
-  
-  
-  
-## Emeritus Committers
-
-Committers who are no longer active include:
+   <p>
+      Loading data, please wait...<br/>
+      <img src="images/loader.gif" alt=""/>
+   </p>
+   <p id="progress"></p>
+   <noscript>
+      <h2>Notice!</h2>
+      <p>
+         This site relies heavily on JavaScript.
+         Please enable it or get a browser that supports it.
+      </p>
+   </noscript>
+    
+   </div>
+   <script type='text/javascript' src="javascripts/phonebook.js" onload="preRender()"></script>