Comprehensive how-to/FAQs for member's meetings
diff --git a/www/members/meeting-util.rb b/www/members/meeting-util.rb
index 0d7c87b..ab34ad5 100644
--- a/www/members/meeting-util.rb
+++ b/www/members/meeting-util.rb
@@ -1,9 +1,23 @@
 # Utility methods and structs related to Member's Meetings
-# NOTE: Assumes 21st century
+# NOTE: Assumes 21st century '2*'
 require 'json'
 
 class MeetingUtil
   RECORDS = 'https://svn.apache.org/repos/private/foundation/Meetings'
+  MEETING_FILES = { # Filename in meeting dir, or pathname to another tool
+    'README.txt' => 'README For Meeting Process',
+    'nomination_of_board.txt' => 'How To Nominate Someone For Board',
+    'nomination_of_members.txt' => 'How To Nominate A New Member',
+    '/members/proxy.cgi' => 'How To Submit A Proxy/Check Your Proxies',
+    'agenda.txt' => 'Official Meeting Agenda',
+    'board_ballot.txt' => 'Official Board Candidate Ballots',
+    'proxies' => 'Official List Of Meeting Proxies',
+    'record' => 'Official List Of Voting Members',
+    'attend' => 'Official List Of Meeting Attendees (afterwards)',
+    'voter-tally' => 'Official List Of Who Voted (afterwards)',
+    'raw_board_votes.txt' => 'Official List Of Votes For Board (afterwards)'
+  }
+
   # Calculate how many members required to attend first half for quorum
   def self.calculate_quorum(mtg_dir)
     begin
diff --git a/www/members/meeting.cgi b/www/members/meeting.cgi
new file mode 100755
index 0000000..8ac15ca
--- /dev/null
+++ b/www/members/meeting.cgi
@@ -0,0 +1,120 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Member's Meeting Information" # Wvisible:meeting
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'whimsy/asf'
+require 'wunderbar/bootstrap'
+require 'date'
+require 'json'
+require 'wunderbar/jquery/stupidtable'
+require_relative 'meeting-util'
+
+# Output action links for meeting records, depending on if current or past
+def emit_meeting(meeting, active)
+  _div id: "meeting-#{meeting}"
+  _whimsy_panel_table(
+    title: "Meeting Details for #{meeting}",
+    helpblock: -> {
+      _p active ? "Live links to the upcoming meeting records/how-tos below." : "These are historical links to the past meeting's record."
+    }
+  ) do
+    _ul do
+      MeetingUtil::MEETING_FILES.each do |f, desc|
+        _li do # Note: cheezy path detection within MEETING_FILES
+          _a desc, href: f.include?('/') ? f : File.join(MeetingUtil::RECORDS, meeting, f)
+        end
+      end
+    end
+  end
+end
+
+# produce HTML
+_html do
+  _body? do
+    MEETINGS = ASF::SVN['Meetings']
+    cur_mtg_dir = MeetingUtil.get_latest(MEETINGS).untaint
+    meeting = File.basename(cur_mtg_dir)
+    mtg_date = Date.parse(meeting)
+    today = Date.today.strftime('%Y%m%d')
+    
+    ROSTER = "/roster/committer"
+    _whimsy_body(
+      title: PAGETITLE,
+      subtitle: 'Meeting How-Tos',
+      relatedtitle: 'More About Meetings',
+      related: {
+        'https://www.apache.org/foundation/governance/meetings' => 'How Meetings & Voting Works',
+        '/members/proxy' => 'Assign A Proxy For Next Meeting',
+        '/members/non-participants' => 'Members Not Participating',
+        '/members/inactive' => 'Inactive Member Feedback Form',
+        MeetingUtil::RECORDS => 'Official Past Meeting Records'
+      },
+      helpblock: -> {
+        if today > meeting
+          _p do
+            _ %{
+              The last Annual Member's Meeting was held #{mtg_date.strftime('%A, %d %B %Y')}.  Expect the 
+              next Member's meeting to be scheduled between 12 - 13 months after 
+              the previous meeting, as per 
+            }
+            _a 'https://www.apache.org/foundation/bylaws.html#3.2', 'the bylaws.'
+            _ 'Stay tuned for a NOTICE email on members@ announcing the next meeting.  The below information is about the '
+            _span.text_warning 'LAST'
+            _ " Member's meeting."
+          end
+        else
+          _p do
+            _ "The next Member's Meeting will start on #{mtg_date.strftime('%A, %d %B %Y')}, as an online meeting on IRC, and will finish up two days later after voting via email is held."
+            _ 'For more details, read on below, or see the links to the right.'
+          end
+        end
+      }
+    ) do
+      help, copypasta = MeetingUtil.is_user_proxied(cur_mtg_dir, $USER)
+      attendance = JSON.parse(IO.read(File.join(MEETINGS, 'attendance.json')))
+      _whimsy_panel("Your Details For Meeting #{meeting}", style: 'panel-info') do
+        # TODO: remind member to check their committer.:email_forward address is correct (where ballots are sent)
+        _p do
+          if help
+            _p help
+            if copypasta
+              _ul.bg_success do
+                copypasta.each do |copyline|
+                  _pre copyline
+                end
+              end
+            end
+          else
+            _ 'You are neither a proxy for anyone else, nor do you appear to have assigned a proxy for your attendance.'
+          end
+        end
+      end
+      
+      emit_meeting(meeting, meeting >= today)
+      
+      _whimsy_panel("Member Meeting History", style: 'panel-info') do
+        all_mtg = Dir[File.join(MEETINGS, '19*'), File.join(MEETINGS, '2*')].sort
+        _p do
+          _ %{ 
+            The ASF has held #{all_mtg.count} Member's meetings in our 
+            history. Some were Annual meetings, were we elect a new board; 
+            a handful were Special mid-year meetings where we mostly just 
+            elected new Members.
+          }
+          _ ' Remember, member meeting minutes are '
+          _span.text_warning 'private'
+          _ ' to the ASF. You can see your '
+          _a 'your own attendance history at meetings.', href: '/members/inactive#attendance'
+          _ul do
+            all_mtg.each do |mtg|
+              _li do
+                tmp = File.join(MeetingUtil::RECORDS, File.basename(mtg))
+                _a tmp, href: tmp
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end