From e195a00d1b8cc306acad381561799e693bfc2fe9 Mon Sep 17 00:00:00 2001
From: Lars Eggert <lars@eggert.org>
Date: Thu, 18 Nov 2021 15:18:48 +0000
Subject: [PATCH] And more agenda fixes.  - Legacy-Id: 19682

---
 ietf/meeting/views.py                         |   6 +-
 ietf/static/js/agenda_filter.js               | 163 ++++---
 ietf/static/js/agenda_personalize.js          |  41 ++
 ietf/static/js/agenda_timezone.js             |  20 +-
 ietf/static/js/timezone.js                    |  42 +-
 ietf/templates/meeting/agenda.html            | 121 ++++--
 .../templates/meeting/agenda_personalize.html | 408 ------------------
 .../agenda_personalize_buttonlist.html        |  24 +-
 ietf/templates/meeting/meeting_heading.html   |  15 +-
 package.json                                  |   1 +
 10 files changed, 271 insertions(+), 570 deletions(-)
 create mode 100644 ietf/static/js/agenda_personalize.js
 delete mode 100644 ietf/templates/meeting/agenda_personalize.html

diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 72161a16b..58499a7d2 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1530,6 +1530,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
     is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
 
     rendered_page = render(request, "meeting/"+base+ext, {
+        "personalize": False,
         "schedule": schedule,
         "filtered_assignments": filtered_assignments,
         "updated": updated,
@@ -1701,8 +1702,9 @@ def agenda_personalize(request, num):
 
     return render(
         request,
-        "meeting/agenda_personalize.html",
+        "meeting/agenda.html",
         {
+            'personalize': True,
             'schedule': meeting.schedule,
             'updated': meeting.updated(),
             'filtered_assignments': filtered_assignments,
@@ -4161,4 +4163,4 @@ def approve_proposed_slides(request, slidesubmission_id, num):
                    'session_number': session_number,
                    'existing_doc' : existing_doc,
                    'form': form,
-                  })
+                  })
\ No newline at end of file
diff --git a/ietf/static/js/agenda_filter.js b/ietf/static/js/agenda_filter.js
index 92da3f600..b84877960 100644
--- a/ietf/static/js/agenda_filter.js
+++ b/ietf/static/js/agenda_filter.js
@@ -3,7 +3,7 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
 
 // closure to create private scope
 (function () {
-    'use strict'
+    'use strict';
 
     /* n.b., const refers to the opts object itself, not its contents.
      * Use camelCase for easy translation into element.dataset keys,
@@ -15,10 +15,10 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
     };
 
     /* Remove from list, if present */
-    function remove_list_item (list, item) {
+    function remove_list_item(list, item) {
         var item_index = list.indexOf(item);
         if (item_index !== -1) {
-            list.splice(item_index, 1)
+            list.splice(item_index, 1);
         }
     }
 
@@ -26,63 +26,68 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
      * 
      * Returns true if added to the list, otherwise false.
      */
-    function toggle_list_item (list, item) {
+    function toggle_list_item(list, item) {
         var item_index = list.indexOf(item);
         if (item_index === -1) {
-            list.push(item)
+            list.push(item);
             return true;
         } else {
-            list.splice(item_index, 1)
+            list.splice(item_index, 1);
             return false;
         }
     }
 
-    function parse_query_params (qs) {
-        var params = {}
-        qs = decodeURI(qs).replace(/^\?/, '').toLowerCase()
+    function parse_query_params(qs) {
+        var params = {};
+        qs = decodeURI(qs)
+            .replace(/^\?/, '')
+            .toLowerCase();
         if (qs) {
-            var param_strs = qs.split('&')
+            var param_strs = qs.split('&');
             for (var ii = 0; ii < param_strs.length; ii++) {
-                var toks = param_strs[ii].split('=', 2)
-                params[toks[0]] = toks[1] || true
+                var toks = param_strs[ii].split('=', 2);
+                params[toks[0]] = toks[1] || true;
             }
         }
-        return params
+        return params;
     }
 
     /* filt = 'show' or 'hide' */
-    function get_filter_from_qparams (qparams, filt) {
+    function get_filter_from_qparams(qparams, filt) {
         if (!qparams[filt] || (qparams[filt] === true)) {
             return [];
         }
         var result = [];
         var qp = qparams[filt].split(',');
-        
+
         for (var ii = 0; ii < qp.length; ii++) {
             result.push(qp[ii].trim());
         }
         return result;
     }
 
-    function get_filter_params (qparams) {
+    function get_filter_params(qparams) {
         var enabled = opts.alwaysShow || qparams.show || qparams.hide;
         return {
             enabled: enabled,
             show: get_filter_from_qparams(qparams, 'show'),
             hide: get_filter_from_qparams(qparams, 'hide')
-        }
+        };
     }
 
     function get_keywords(elt) {
-        var keywords = $(elt).attr('data-filter-keywords');
+        var keywords = $(elt)
+            .attr('data-filter-keywords');
         if (keywords) {
-            return keywords.toLowerCase().split(',');
+            return keywords.toLowerCase()
+                .split(',');
         }
         return [];
     }
 
     function get_item(elt) {
-        return $(elt).attr('data-filter-item');
+        return $(elt)
+            .attr('data-filter-item');
     }
 
     // utility method - is there a match between two lists of keywords?
@@ -94,25 +99,27 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
         }
         return false;
     }
-    
+
     // Find the items corresponding to a keyword
-    function get_items_with_keyword (keyword) {
+    function get_items_with_keyword(keyword) {
         var items = [];
 
-        $('.view button.pickview').filter(function(index, elt) {
-            return keyword_match(get_keywords(elt), [keyword]);
-        }).each(function (index, elt) {
-            items.push(get_item($(elt)));
-        });
+        $('.view button.pickview')
+            .filter(function (index, elt) {
+                return keyword_match(get_keywords(elt), [keyword]);
+            })
+            .each(function (index, elt) {
+                items.push(get_item($(elt)));
+            });
         return items;
     }
 
-    function filtering_is_enabled (filter_params) {
+    function filtering_is_enabled(filter_params) {
         return filter_params.enabled;
     }
 
     // Update the filter / customization UI to match the current filter parameters
-    function update_filter_ui (filter_params) {
+    function update_filter_ui(filter_params) {
         var buttons = $('.pickview');
 
         if (!filtering_is_enabled(filter_params)) {
@@ -121,12 +128,12 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
             return;
         }
 
-        update_href_querystrings(filter_params_as_querystring(filter_params))
+        update_href_querystrings(filter_params_as_querystring(filter_params));
 
         // show the customizer - it will stay visible even if filtering is disabled
         const customizer = $('#customize');
         if (customizer.hasClass('collapse')) {
-            customizer.collapse('show')
+            customizer.collapse('show');
         }
 
         // Update button state to match visibility
@@ -135,7 +142,7 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
             var keywords = get_keywords(elt);
             keywords.push(get_item(elt)); // treat item as one of its keywords
             var hidden = keyword_match(filter_params.hide, keywords);
-            var shown = keyword_match(filter_params.show, keywords); 
+            var shown = keyword_match(filter_params.show, keywords);
             if (shown && !hidden) {
                 elt.addClass('active');
             } else {
@@ -149,11 +156,11 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
      * Calling the individual update_* functions outside of this method will likely cause
      * various parts of the page to get out of sync.
      */
-    function update_view () {
-        var filter_params = get_filter_params(parse_query_params(window.location.search))
-        update_filter_ui(filter_params)
+    function update_view() {
+        var filter_params = get_filter_params(parse_query_params(window.location.search));
+        update_filter_ui(filter_params);
         if (opts.updateCallback) {
-            opts.updateCallback(filter_params)
+            opts.updateCallback(filter_params);
         }
     }
 
@@ -162,19 +169,19 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
      * Updates the URL to match filter_params, then updates the history / display to match
      * (if supported) or loads the new URL.
      */
-    function update_filters (filter_params) {
+    function update_filters(filter_params) {
         var new_url = replace_querystring(
-          window.location.href,
-          filter_params_as_querystring(filter_params)
-        )
-        update_href_querystrings(filter_params_as_querystring(filter_params))
+            window.location.href,
+            filter_params_as_querystring(filter_params)
+        );
+        update_href_querystrings(filter_params_as_querystring(filter_params));
         if (window.history && window.history.replaceState) {
             // Keep current origin, replace search string, no page reload
-            history.replaceState({}, document.title, new_url)
-            update_view()
+            history.replaceState({}, document.title, new_url);
+            update_view();
         } else {
             // No window.history.replaceState support, page reload required
-            window.location = new_url
+            window.location = new_url;
         }
     }
 
@@ -183,34 +190,35 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
      */
     function update_href_querystrings(querystring) {
         Array.from(
-          document.getElementsByClassName('agenda-link filterable')
-        ).forEach(
-          (elt) => elt.href = replace_querystring(elt.href, querystring)
-        )
+                document.getElementsByClassName('agenda-link filterable')
+            )
+            .forEach(
+                (elt) => elt.href = replace_querystring(elt.href, querystring)
+            );
     }
 
     function filter_params_as_querystring(filter_params) {
-        var qparams = []
+        var qparams = [];
         if (filter_params.show.length > 0) {
-            qparams.push('show=' + filter_params.show.join())
+            qparams.push('show=' + filter_params.show.join());
         }
         if (filter_params.hide.length > 0) {
-            qparams.push('hide=' + filter_params.hide.join())
+            qparams.push('hide=' + filter_params.hide.join());
         }
         if (qparams.length > 0) {
-            return '?' + qparams.join('&')
+            return '?' + qparams.join('&');
         }
-        return ''
+        return '';
     }
 
     function replace_querystring(url, new_querystring) {
-        return url.replace(/(\?.*)?(#.*)?$/, new_querystring + window.location.hash)
+        return url.replace(/(\?.*)?(#.*)?$/, new_querystring + window.location.hash);
     }
 
     /* Helper for pick group/type button handlers - toggles the appropriate parameter entry
      *    elt - the jquery element that was clicked
      */
-    function handle_pick_button (elt) {
+    function handle_pick_button(elt) {
         var fp = get_filter_params(parse_query_params(window.location.search));
         var item = get_item(elt);
 
@@ -232,17 +240,17 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
          * an area will enable all items in the row as one would expect. */
         if (just_showed_item) {
             var children = get_items_with_keyword(item);
-            $.each(children, function(index, child) {
+            $.each(children, function (index, child) {
                 remove_list_item(fp.show, child);
                 remove_list_item(fp.hide, child);
             });
         }
-        
+
         // If the show list is empty, clear the hide list because there is nothing to hide
         if (fp.show.length === 0) {
             fp.hide = [];
         }
-        
+
         return fp;
     }
 
@@ -251,23 +259,13 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
     }
 
     function register_handlers() {
-        $('.pickview').on("click", function () {
-            if (is_disabled($(this))) { return; }
-            var fp = handle_pick_button($(this));
-            update_filters(fp);
-        });
-    }
-
-    /**
-     * Read options from the template
-     */
-    function read_template_options() {
-        const opts_elt = document.getElementById('agenda-filter-options');
-        opts.keys().forEach((opt) => {
-            if (opt in opts_elt.dataset) {
-                opts[opt] = opts_elt.dataset[opt];
-            }
-        });
+        $('.pickview')
+            .on("click", function () {
+                console.log("pickview");
+                if (is_disabled($(this))) { return; }
+                var fp = handle_pick_button($(this));
+                update_filters(fp);
+            });
     }
 
     /* Entry point to filtering code when page loads
@@ -275,17 +273,18 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
      * This must be called if you are using the HTML template to provide a customization
      * button UI. Do not call if you only want to use the parameter parsing routines.
      */
-    function enable () {
+    function enable() {
         // ready handler fires immediately if document is already "ready"
-        $(document).ready(function () {
-            register_handlers();
-            update_view();
-        })
+        $(document)
+            .ready(function () {
+                register_handlers();
+                update_view();
+            })
     }
 
     // utility method - filter a jquery set to those matching a keyword
     function rows_matching_filter_keyword(rows, kw) {
-        return rows.filter(function(index, element) {
+        return rows.filter(function (index, element) {
             var row_kws = get_keywords(element);
             return keyword_match(row_kws, [kw.toLowerCase()]);
         });
@@ -305,6 +304,6 @@ window.agenda_filter_for_testing; // methods to be accessed for automated testin
         keyword_match: keyword_match,
         parse_query_params: parse_query_params,
         rows_matching_filter_keyword: rows_matching_filter_keyword,
-        set_update_callback: function (cb) {opts.updateCallback = cb}
+        set_update_callback: function (cb) { opts.updateCallback = cb }
     };
 })();
\ No newline at end of file
diff --git a/ietf/static/js/agenda_personalize.js b/ietf/static/js/agenda_personalize.js
new file mode 100644
index 000000000..edb2787f3
--- /dev/null
+++ b/ietf/static/js/agenda_personalize.js
@@ -0,0 +1,41 @@
+// Copyright The IETF Trust 2021, All Rights Reserved
+
+/**
+ * Agenda personalization JS methods
+ *
+ * Requires agenda_timezone.js and timezone.js be included.
+ */
+'use strict';
+
+/**
+ * Update the checkbox state to match the filter parameters
+ */
+function updateAgendaCheckboxes(filter_params) {
+    var selection_inputs = document.getElementsByName('selected-sessions');
+    selection_inputs.forEach((inp) => {
+        const item_keywords = inp.dataset.filterKeywords.toLowerCase()
+            .split(',');
+        if (
+            agenda_filter.keyword_match(item_keywords, filter_params.show) &&
+            !agenda_filter.keyword_match(item_keywords, filter_params.hide)
+        ) {
+            inp.checked = true;
+        } else {
+            inp.checked = false;
+        }
+    });
+}
+
+window.handleFilterParamUpdate = function (filter_params) {
+    updateAgendaCheckboxes(filter_params);
+};
+
+window.handleTableClick = function (event) {
+    if (event.target.name === 'selected-sessions') {
+        // hide the tooltip after clicking on a checkbox
+        const jqElt = jQuery(event.target);
+        if (jqElt.tooltip) {
+            jqElt.tooltip('hide');
+        }
+    }
+};
\ No newline at end of file
diff --git a/ietf/static/js/agenda_timezone.js b/ietf/static/js/agenda_timezone.js
index bef58d6cb..5b8907c83 100644
--- a/ietf/static/js/agenda_timezone.js
+++ b/ietf/static/js/agenda_timezone.js
@@ -13,12 +13,12 @@ var local_timezone = moment.tz.guess();
 
 // get_current_tz_cb must be overwritten using set_current_tz_cb
 function get_current_tz_cb() {
-    throw new Error('Tried to get current timezone before callback registered. Use set_current_tz_cb().')
+    throw new Error('Tried to get current timezone before callback registered. Use set_current_tz_cb().');
 };
 
 // Initialize moments
 window.initialize_moments = function () {
-    var times = $('div.time')
+    var times = $('div.time');
     $.each(times, function (i, item) {
         item.start_ts = moment.unix(this.getAttribute("data-start-time"))
             .utc();
@@ -33,7 +33,7 @@ window.initialize_moments = function () {
             item.format = +this.getAttribute("format");
         }
     });
-    var times = $('[data-slot-start-ts]')
+    times = $('[data-slot-start-ts]');
     $.each(times, function (i, item) {
         item.slot_start_ts = moment.unix(this.getAttribute("data-slot-start-ts"))
             .utc();
@@ -205,7 +205,7 @@ window.update_times = function (newtz) {
         });
     update_tooltips_all();
     update_clock();
-}
+};
 
 // Highlight ongoing based on the current time
 window.highlight_ongoing = function () {
@@ -213,7 +213,7 @@ window.highlight_ongoing = function () {
         .remove("#now");
     $('.table-warning')
         .removeClass("table-warning");
-    var agenda_rows = $('[data-slot-start-ts]')
+    var agenda_rows = $('[data-slot-start-ts]');
     agenda_rows = agenda_rows.filter(function () {
         return moment()
             .isBetween(this.slot_start_ts, this.slot_end_ts);
@@ -245,13 +245,13 @@ window.update_tooltips_all = function () {
         $(this)
             .html(format_tooltip_table(this.start_ts, this.end_ts));
     });
-}
+};
 
 // Update clock
 window.update_clock = function () {
     $('#current-time')
         .html(format_time(moment(), get_current_tz_cb(), 0));
-}
+};
 
 $.urlParam = function (name) {
     var results = new RegExp('[\?&]' + name + '=([^&#]*)')
@@ -261,7 +261,7 @@ $.urlParam = function (name) {
     } else {
         return results[1] || 0;
     }
-}
+};
 
 window.init_timers = function () {
     var fast_timer = 60000 / (speedup > 600 ? 600 : speedup);
@@ -271,9 +271,9 @@ window.init_timers = function () {
     setInterval(function () { highlight_ongoing(); }, fast_timer);
     setInterval(function () { update_tooltips(); }, fast_timer);
     setInterval(function () { update_tooltips_all(); }, 3600000 / speedup);
-}
+};
 
 // set method used to find current time zone
 window.set_current_tz_cb = function (fn) {
     get_current_tz_cb = fn;
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/ietf/static/js/timezone.js b/ietf/static/js/timezone.js
index fda6f7f47..5f77dccf3 100644
--- a/ietf/static/js/timezone.js
+++ b/ietf/static/js/timezone.js
@@ -17,18 +17,19 @@ window.ietf_timezone; // public interface
     var current_timezone;
 
     // Select timezone to use. Arg is name of a timezone or 'local' to guess local tz.
-    function use_timezone (newtz) {
+    function use_timezone(newtz) {
         // Guess local timezone if necessary
         if (newtz.toLowerCase() === 'local') {
-            newtz = moment.tz.guess()
+            newtz = moment.tz.guess();
         }
 
         if (current_timezone !== newtz) {
-            current_timezone = newtz
+            current_timezone = newtz;
             // Update values of tz-select inputs but do not trigger change event
-            $('select.tz-select').val(newtz)
+            $('select.tz-select')
+                .val(newtz);
             if (timezone_change_callback) {
-                timezone_change_callback(newtz)
+                timezone_change_callback(newtz);
             }
         }
     }
@@ -38,37 +39,40 @@ window.ietf_timezone; // public interface
      * This will set the timezone to the value of 'current'. Set up the tz_change callback
      * before initializing.
      */
-    function timezone_init (current) {
-        var tz_names = moment.tz.names()
-        var select = $('select.tz-select')
+    function timezone_init(current) {
+        var tz_names = moment.tz.names();
+        var select = $('select.tz-select');
 
-        select.empty()
+        select.empty();
         $.each(tz_names, function (i, item) {
             if (current === item) {
                 select.append($('<option/>', {
-                    selected: 'selected', html: item, value: item
-                }))
+                    selected: 'selected',
+                    html: item,
+                    value: item
+                }));
             } else {
                 select.append($('<option/>', {
-                    html: item, value: item
-                }))
+                    html: item,
+                    value: item
+                }));
             }
-        })
-        select.on("change", function () { use_timezone(this.value)});
+        });
+        select.on("change", function () { use_timezone(this.value); });
         /* When navigating back/forward, the browser may change the select input's
          * value after the window load event. It does not fire the change event on
          * the input when it does this. The pageshow event occurs after such an update,
          * so trigger the change event ourselves to be sure the UI stays consistent
          * with the timezone select input. */
-        window.addEventListener('pageshow', function(){select.trigger("change"); })
+        window.addEventListener('pageshow', function () { select.trigger("change"); });
         use_timezone(current);
     }
 
     // Expose public interface
     ietf_timezone = {
-        get_current_tz: function() {return current_timezone},
+        get_current_tz: function () { return current_timezone },
         initialize: timezone_init,
-        set_tz_change_callback: function(cb) {timezone_change_callback=cb},
+        set_tz_change_callback: function (cb) { timezone_change_callback = cb; },
         use: use_timezone
-    }
+    };
 })();
\ No newline at end of file
diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html
index d8459183b..824b22d54 100644
--- a/ietf/templates/meeting/agenda.html
+++ b/ietf/templates/meeting/agenda.html
@@ -4,13 +4,16 @@
 {% load static %}
 {% load ietf_filters %}
 {% load textfilters %}
-{% load htmlfilters agenda_custom_tags%}
+{% load htmlfilters agenda_custom_tags %}
 
 {% block title %}
     IETF {{ schedule.meeting.number }} Meeting Agenda
     {% if "-utc" in request.path %}
         (UTC)
     {% endif %}
+    {% if personalize %}
+        Personalization
+    {% endif %}
 {% endblock %}
 
 {% block morecss %}
@@ -24,9 +27,10 @@
 
     <div class="row">
         <div class="col-md-10">
-
             {% if "-utc" in request.path %}
                 {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda-utc" title_extra="(UTC)" %}
+            {% elif personalize %}
+                {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="select-sessions" title_extra="" %}
             {% else %}
                 {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda" title_extra="" %}
             {% endif %}
@@ -34,9 +38,16 @@
             {# cache this part -- it takes 3-6 seconds to generate #}
             {% load cache %}
             {% cache cache_time ietf_meeting_agenda_utc schedule.meeting.number request.path %}
+
                 <div class="row">
                     <div class="col-5">
-                        <h2>Agenda</h2>
+                        <h2>
+                            {% if personalize %}
+                                Session Selection
+                            {% else %}
+                                Agenda
+                            {% endif %}
+                        </h2>
                     </div>
                     <div class="col float-end tz-display">
                         <div class="input-group input-group-sm">
@@ -68,28 +79,22 @@
                 {% endif %}
 
                 <p>
-                    {% include "meeting/agenda_filter.html" with filter_categories=filter_categories customize_button_text="Customize the agenda view..." %}
+                    {% include "meeting/agenda_filter.html" with filter_categories=filter_categories customize_button_text="Personalize the agenda view..." always_show=personalize%}
                 </p>
 
-                <p>
-                    <div class="d-inline mb-3">Download agenda as .ics:</div>
+                {% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting only %}
 
-                    <div class="d-inline">
-                        <a id="ical-link" class="visually-hidden btn btn-sm btn-primary agenda-link filterable" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}">Customized schedule above</a>
-                    </div>
-
-                    <div class="input-group input-group-sm mb-3 d-inline">
-                        <button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">By area</button><ul class="dropdown-menu">
-                            {% for fc in filter_categories %}
-                                {% if not forloop.last %} {# skip the last group, it's the office hours/misc #}
-                                    {% for p in fc|dictsort:"label" %}
-                                        <li><a class="dropdown-item" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{p.keyword}}">{{p.label}}</a></li>
-                                    {% endfor %}
-                                {% endif %}
-                            {% endfor %}
-                        </ul><a class="btn btn-outline-primary" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{ non_area_keywords|join:',' }}">Non-area events</a>
-                    </div>
-                </p>
+                <div class="input-group input-group-sm mb-3">
+                    <button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Download area agenda</button><ul class="dropdown-menu">
+                        {% for fc in filter_categories %}
+                            {% if not forloop.last %} {# skip the last group, it's the office hours/misc #}
+                                {% for p in fc|dictsort:"label" %}
+                                    <li><a class="dropdown-item" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{p.keyword}}">{{p.label}}</a></li>
+                                {% endfor %}
+                            {% endif %}
+                        {% endfor %}
+                    </ul><a class="btn btn-outline-primary" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{ non_area_keywords|join:',' }}">Download non-area events</a>
+                </div>
 
                 <div id="weekview" class="visually-hidden mt-3">
                     <h2>
@@ -103,14 +108,22 @@
                     <iframe class="w-100 overflow-hidden border border-dark" scrolling="no"></iframe>
                 </div>
 
-                <h2>Detailed Agenda</h2>
+                <h2 class="mt-3">
+                    {% if personalize %}
+                        Personalize
+                    {% endif %}
+                    Detailed Agenda
+                </h2>
+                {% if personalize %}
+                    <p>Check boxes below to select individual sessions.</p>
+                {% endif %}
 
-                <table class="table table-sm">
+                <table id="agenda-table" class="table table-sm">
                     {% for item in filtered_assignments %}
 
                         {% ifchanged item.timeslot.time|date:"Y-m-d" %}
                             <tr class="table-primary">
-                                <th colspan="5">
+                                <th colspan="6">
                                     {# The anchor here needs to be in a div, not in the th, in order for the anchor-target margin hack to work #}
                                     <div class="anchor-target" id="slot-{{item.timeslot.time|slugify}}"></div>
                                     <div class="h6 mt-2">{{ item.timeslot.time|date:"l, F j, Y" }}</div>
@@ -122,7 +135,20 @@
                             <tr id="row-{{ item.slug }}" data-filter-keywords="{{ item.filter_keywords|join:',' }}"
                                 data-slot-start-ts="{{item.start_timestamp}}"
                                 data-slot-end-ts="{{item.end_timestamp}}">
-                                <td class="text-nowrap text-right">
+                                <td class="text-center">
+                                    {% if item.session_keyword %}
+                                        <input
+                                            type="checkbox"
+                                            class="pickview form-check-input"
+                                            title="Select session"
+                                            name="selected-sessions"
+                                            value="{{ item.session_keyword }}"
+                                            data-filter-keywords="{{ item.filter_keywords|join:',' }}"
+                                            data-filter-item="{{ item.session_keyword }}">
+                                    {% endif %}
+                                </td>
+
+                                <td class="text-nowrap text-end">
                                     {% include "meeting/timeslot_start_end.html" %}
                                 </td>
                                 <td colspan="3">
@@ -171,7 +197,10 @@
                         <tr class="table-secondary session-label-row"
                             data-slot-start-ts="{{item.start_timestamp}}"
                             data-slot-end-ts="{{item.end_timestamp}}">
-                            <th class="text-nowrap text-right">
+                            <td class="text-center">
+                            </td>
+
+                            <th class="text-nowrap text-end">
                                 {% include "meeting/timeslot_start_end.html" %}
                             </th>
                             <th colspan="4">
@@ -188,8 +217,22 @@
                         data-filter-keywords="{{ item.filter_keywords|join:',' }}"
                         data-slot-start-ts="{{item.start_timestamp}}"
                         data-slot-end-ts="{{item.end_timestamp}}">
+
+                        <td class="text-center">
+                            {% if item.session_keyword %}
+                                <input
+                                    type="checkbox"
+                                    class="pickview form-check-input"
+                                    title="Select session"
+                                    name="selected-sessions"
+                                    value="{{ item.session_keyword }}"
+                                    data-filter-keywords="{{ item.filter_keywords|join:',' }}"
+                                    data-filter-item="{{ item.session_keyword }}">
+                            {% endif %}
+                        </td>
+
                         {% if item.slot_type.slug == 'plenary' %}
-                            <th class="text-nowrap text-right">
+                            <th class="text-nowrap text-end">
                                 {% include "meeting/timeslot_start_end.html" %}
                             </th>
                             <td colspan="3">
@@ -270,6 +313,8 @@
 {% endif %}
 {% endfor %}
 </table>
+{% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting only %}
+
 </div>
 
 <div class="col-md-2 d-print-none" id="affix">
@@ -341,8 +386,7 @@
         }
 
         function update_ical_links(filter_params) {
-            var ical_link = $("#ical-link");
-            ical_link.toggleClass("visually-hidden", !agenda_filter.filtering_is_enabled(filter_params));
+            $(".ical-link").toggleClass("visually-hidden", !agenda_filter.filtering_is_enabled(filter_params));
         }
 
         function update_weekview(filter_params) {
@@ -389,6 +433,10 @@
     <script src="{% static 'ietf/js/timezone.js' %}"></script>
     <script src="{% static 'ietf/js/agenda_materials.js' %}"></script>
     <script src="{% static 'ietf/js/agenda_timezone.js' %}"></script>
+    {% if personalize %}
+        <script src="{% static 'ietf/js/agenda_personalize.js' %}"></script>
+    {% endif %}
+
     <script>
         {% if settings.DEBUG and settings.DEBUG_AGENDA %}
             speedup = +$.urlParam('speedup');
@@ -441,7 +489,18 @@
             init_timers();
 
             // Finally, set up the agenda filter UI. This does not depend on the timezone.
-            agenda_filter.set_update_callback(update_view);
+            {% if personalize %}
+                agenda_filter.set_update_callback(function (e) {
+                    handleFilterParamUpdate(e);
+                    update_view(e);
+                });
+
+                document.getElementById('agenda-table')
+                .addEventListener('click', handleTableClick);
+            {% else %}
+                agenda_filter.set_update_callback(update_view);
+            {% endif %}
+
             agenda_filter.enable();
         }
         );
diff --git a/ietf/templates/meeting/agenda_personalize.html b/ietf/templates/meeting/agenda_personalize.html
deleted file mode 100644
index f208ef8ec..000000000
--- a/ietf/templates/meeting/agenda_personalize.html
+++ /dev/null
@@ -1,408 +0,0 @@
-{% extends "base.html" %}
-{# Copyright The IETF Trust 2021, All Rights Reserved #}
-{% load origin %}
-{% load static %}
-{% load ietf_filters %}
-{% load textfilters %}
-{% load htmlfilters %}
-
-{% block title %}
-    IETF {{ schedule.meeting.number }} meeting agenda personalization
-{% endblock %}
-
-{% block morecss %}
-    tr:not(:first-child) th.gap {
-    height: 3em !important;
-    background-color: inherit !important;
-    border: none !important;
-    }
-    tr:first-child th.gap {
-    height: 0 !important;
-    background-color: inherit !important;
-    border: none !important;
-    }
-    div.tz-display {
-    margin-bottom: 0.5em;
-    margin-top: 1em;
-    text-align: right;
-    }
-    .tz-display a {
-    cursor: pointer;
-    }
-    .tz-display label {
-    font-weight: normal;
-    }
-    .tz-display select {
-    min-width: 15em;
-    }
-    #affix .nav li.tz-display {
-    padding: 4px 20px;
-    }
-    #affix .nav li.tz-display a {
-    display: inline;
-    padding: 0;
-    }
-{% endblock %}
-
-{% block bodyAttrs %}data-bs-spy="scroll" data-bs-target="#affix"{% endblock %}
-
-{% block content %}
-    {% origin %}
-
-    <div class="row">
-        <div class="col-md-12">
-            {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="select-sessions" title_extra="" %}
-        </div>
-    </div>
-    <div class="row">
-        <div class="col-md-10">
-            {# cache this part -- it takes 3-6 seconds to generate #}
-            {% load cache %}
-            {% cache cache_time ietf_meeting_agenda_personalize schedule.meeting.number request.path %}
-                <div class="row">
-                    <div class="col-xs-6"><h1>Session Selection</h1></div>
-                    <div class="col-xs-6">
-                        <div class="tz-display">
-                            <div><small>
-                                <label for="timezone-select">Time zone:</label>
-                                <a id="meeting-timezone" onclick="ietf_timezone.use('{{ timezone }}')">Meeting</a> |
-                                <a id="local-timezone" onclick="ietf_timezone.use('local')">Local</a> |
-                                <a id="utc-timezone" onclick="ietf_timezone.use('UTC')">UTC</a>
-                            </small></div>
-                            <select id="timezone-select" class="tz-select">
-                                {# Avoid blank while loading. JavaScript replaces the option list after init. #}
-                                <option selected>{{ timezone }}</option>
-                            </select>
-                        </div>
-                    </div>
-                </div>
-                {% if is_current_meeting %}
-                    <p class="alert alert-info">
-                        <b>Note:</b> IETF agendas are subject to change, up to and during a meeting.
-                    </p>
-                {% endif %}
-
-                {% include "meeting/agenda_filter.html" with filter_categories=filter_categories always_show=True %}
-
-                {% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting only %}
-
-                <h2>
-                    Individual Sessions
-                </h2>
-                <p>
-                    Check boxes below to select individual sessions.
-                </p>
-
-                <table id="agenda-table" class="table table-sm table-striped">
-                    {% for item in filtered_assignments %}
-
-                        {% ifchanged item.timeslot.time|date:"Y-m-d" %}
-                            <tr>
-                                <th class="gap" colspan="7"></th>
-                            </tr>
-                            <tr class="table-warning">
-                                <th colspan="7">
-                                    {# The anchor here needs to be in a div, not in the th, in order for the anchor-target margin hack to work #}
-                                    <div class="anchor-target" id="slot-{{ item.timeslot.time|slugify }}"></div>
-                                    {{ item.timeslot.time|date:"l, F j, Y" }}
-                                </th>
-                            </tr>
-                        {% endifchanged %}
-
-                        {% if item.timeslot.type_id == 'regular' %}
-                            {% ifchanged %}
-                                <tr class="table-info session-label-row"
-                                    data-slot-start-ts="{{ item.start_timestamp }}"
-                                    data-slot-end-ts="{{ item.end_timestamp }}">
-                                    <td class="leftmarker"></td>
-                                    <th class="text-nowrap text-right">
-                                        <span class="d-none d-sm-block">
-                                            {% include "meeting/timeslot_start_end.html" %}
-                                        </span>
-                                    </th>
-                                    <th colspan="4">
-                                        <span class="d-none d-md-block d-lg-block d-xl-block d-xxl-block">
-                                            {% include "meeting/timeslot_start_end.html" %}
-                                        </span>
-                                        {{ item.timeslot.time|date:"l" }}
-                                        {{ item.timeslot.name|capfirst_allcaps }}
-                                    </th>
-                                    <td class="rightmarker"></td>
-                                </tr>
-                            {% endifchanged %}
-                        {% endif %}
-
-
-                        {% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %}
-                            <tr id="row-{{ item.slug }}"
-                                data-slot-start-ts="{{ item.start_timestamp }}"
-                                data-slot-end-ts="{{ item.end_timestamp }}">
-                                <td class="leftmarker">
-                                    {% if item.session_keyword %}
-                                        <input
-                                            type="checkbox"
-                                            class="pickview"
-                                            title="Select session"
-                                            name="selected-sessions"
-                                            value="{{ item.session_keyword }}"
-                                            data-filter-keywords="{{ item.filter_keywords|join:',' }}"
-                                            data-filter-item="{{ item.session_keyword }}">
-                                    {% endif %}
-                                </td>
-                                <td class="text-nowrap text-right">
-                                    <span class="d-none d-sm-block">
-                                        {% include "meeting/timeslot_start_end.html" %}
-                                    </span>
-                                </td>
-                                <td colspan="3">
-                                    <span class="d-none d-md-block d-lg-block d-xl-block d-xxl-block">
-                                        {% include "meeting/timeslot_start_end.html" %}
-                                    </span>
-                                    {% if item.timeslot.show_location and item.timeslot.get_html_location %}
-                                        {% if schedule.meeting.number|add:"0" < 96 %}
-                                            {% comment %}<a href="https://tools.ietf.org/agenda/{{ schedule.meeting.number }}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment $}
-                      {{ item.timeslot.get_html_location }}
-                      {% comment %}</a>{% endcomment %}
-                                        {% elif item.timeslot.location.floorplan %}
-                                            <a
-                                                href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{ item.timeslot.get_html_location }}</a>
-                                        {% else %}
-                                            {{ item.timeslot.get_html_location }}
-                                        {% endif %}
-                                        {% with item.timeslot.location.floorplan as floor %}
-                                            {% if item.timeslot.location.floorplan %}
-                                                <span class="d-none d-sm-block">
-                                                    <a
-                                                        href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#{{ floor.name|xslugify }}"
-                                                        class="float-end" title="{{ floor.name }}"><span
-                                                            class="badge bg-blank label-wide">{{ floor.short }}</span></a>
-                                                </span>
-                                            {% endif %}
-                                        {% endwith %}
-                                    {% endif %}
-                                </td>
-                                <td>
-                                    {% if item.session.agenda %}
-                                        <a href="{{ item.session.agenda.get_href }}">
-                                            {{ item.timeslot.name }}
-                                        </a>
-                                    {% else %}
-                                        {{ item.timeslot.name }}
-                                    {% endif %}
-
-                                    {% if item.session.current_status == 'canceled' %}
-                                        <span class="badge bg-danger float-end">CANCELLED</span>
-                                    {% endif %}
-                                </td>
-                                <td class="rightmarker"></td>
-                            </tr>
-                        {% endif %}
-
-                        {% if item.timeslot.type_id == 'regular' or item.timeslot.type.slug == 'plenary' %}
-                            {% if item.session.historic_group %}
-                                <tr id="row-{{ item.slug }}"
-                                    {% if item.timeslot.type.slug == 'plenary' %}class="{{ item.timeslot.type.slug }}danger"{% endif %}
-                                    data-slot-start-ts="{{ item.start_timestamp }}"
-                                    data-slot-end-ts="{{ item.end_timestamp }}">
-                                    <td class="leftmarker">
-                                        {% if item.session_keyword %}
-                                            <input
-                                                type="checkbox"
-                                                class="pickview"
-                                                title="Select session"
-                                                name="selected-sessions"
-                                                value="{{ item.session_keyword }}"
-                                                data-filter-keywords="{{ item.filter_keywords|join:',' }}"
-                                                data-filter-item="{{ item.session_keyword }}">
-                                        {% endif %}
-                                    </td>
-                                    {% if item.timeslot.type.slug == 'plenary' %}
-                                        <th class="text-nowrap text-right">
-                                            <span class="d-none d-sm-block">
-                                                {% include "meeting/timeslot_start_end.html" %}
-                                            </span>
-                                        </th>
-                                        <td colspan="3">
-                                            <span class="d-none d-md-block d-lg-block d-xl-block d-xxl-block">
-                                                {% include "meeting/timeslot_start_end.html" %}
-                                            </span>
-                                            {% if item.timeslot.show_location and item.timeslot.get_html_location %}
-                                                {% if schedule.meeting.number|add:"0" < 96 %}
-                                                    {% comment %}<a href="https://tools.ietf.org/agenda/{{ schedule.meeting.number }}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment %}
-                                                    {{ item.timeslot.get_html_location }}
-                                                    {% comment %}</a>{% endcomment %}
-                                                {% elif item.timeslot.location.floorplan %}
-                                                    <a
-                                                        href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{ item.timeslot.get_html_location }}</a>
-                                                {% else %}
-                                                    {{ item.timeslot.get_html_location }}
-                                                {% endif %}
-                                            {% endif %}
-                                        </td>
-
-                                    {% else %}
-                                        <td>
-                                            {% with item.timeslot.location.floorplan as floor %}
-                                                {% if item.timeslot.location.floorplan %}
-                                                    <span class="d-none d-sm-block">
-                                                        <a
-                                                            href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#{{ floor.name|xslugify }}"
-                                                            class="float-end" title="{{ floor.name }}"><span
-                                                                class="badge bg-blank">{{ floor.short }}</span></a>
-                                                    </span>
-                                                {% endif %}
-                                            {% endwith %}
-                                        </td>
-                                        <td>
-                                            {% if item.timeslot.show_location and item.timeslot.get_html_location %}
-                                                {% if schedule.meeting.number|add:"0" < 96 %}
-                                                    {% comment %}<a href="https://tools.ietf.org/agenda/{{ schedule.meeting.number }}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment %}
-                                                    {{ item.timeslot.get_html_location }}
-                                                    {% comment %}</a>{% endcomment %}
-                                                {% elif item.timeslot.location.floorplan %}
-                                                    <a
-                                                        href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{ item.timeslot.get_html_location }}</a>
-                                                {% else %}
-                                                    {{ item.timeslot.get_html_location }}
-                                                {% endif %}
-                                            {% endif %}
-                                        </td>
-
-                                        <td><span class="d-none d-sm-block">{{ item.session.historic_group.historic_parent.acronym }}</span></td>
-
-                                        <td>
-                                            {% if item.session.historic_group %}
-                                                <a
-                                                    href="{% url 'ietf.group.views.group_about' acronym=item.session.historic_group.acronym %}">{{ item.session.historic_group.acronym }}</a>
-                                            {% else %}
-                                                {{ item.session.historic_group.acronym }}
-                                            {% endif %}
-                                        </td>
-                                    {% endif %}
-
-                                    <td>
-                                        {% if item.session.agenda %}
-                                            <a href="{{ item.session.agenda.get_href }}">
-                                        {% endif %}
-                                        {% if item.timeslot.type.slug == 'plenary' %}
-                                            {{ item.timeslot.name }}
-                                        {% else %}
-                                            {{ item.session.historic_group.name }}
-                                        {% endif %}
-                                        {% if item.session.agenda %}
-                                            </a>
-                                        {% endif %}
-
-                                        {% if item.session.current_status == 'canceled' %}
-                                            <span class="badge bg-danger float-end">CANCELLED</span>
-                                        {% endif %}
-
-                                        {% if item.session.historic_group.state_id == "bof" %}
-                                            <span class="badge bg-success float-end">BOF</span>
-                                        {% endif %}
-
-                                        {% if item.session.current_status == 'resched' %}
-                                            <span class="badge bg-danger float-end">
-                                                RESCHEDULED
-                                                {% if item.session.rescheduled_to %}
-                                                    TO
-                                                    <span class="timetooltip reschedtimetooltip"><span class="time"
-                                                        data-start-time="{{ item.session.rescheduled_to.utc_start_time|date:"U" }}"
-                                                        data-end-time="{{ item.session.rescheduled_to.utc_end_time|date:"U" }}"
-                                                        {% if item.timeslot.time|date:"l" != item.session.rescheduled_to.time|date:"l" %}
-                                                            weekday="1"{% endif %}>
-                                                        {% if "-utc" in request.path %}
-                                                            {{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-
-                                                            {{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
-                                                        {% else %}
-                                                            {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-
-                                                            {{ item.session.rescheduled_to.end_time|date:"G:i" }}
-                                                        {% endif %}
-                                                    </span></span>
-                                                {% endif %}
-                                            </span>
-                                        {% endif %}
-
-                                        {% if item.session.agenda_note|first_url|conference_url %}
-                                            <br>
-                                            <a href={{ item.session.agenda_note|first_url }}>{{ item.session.agenda_note|slice:":23" }}</a>
-                                        {% elif item.session.agenda_note %}
-                                            <br><span class="text-danger">{{ item.session.agenda_note }}</span>
-                                        {% endif %}
-                                    </td>
-                                    <td class="rightmarker"></td>
-                                </tr>
-                            {% endif %}
-                        {% endif %}
-                    {% endfor %}
-                </table>
-
-                {% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting only %}
-
-                </div>
-                <div class="col-md-2 d-print-none" id="affix">
-                    <ul class="nav nav-pills nav-stacked small" data-bs-spy="affix">
-                        <li><a href="#now">Now</a></li>
-                        {% for item in filtered_assignments %}
-                            {% ifchanged item.timeslot.time|date:"Y-m-d" %}
-                                <li><a href="#slot-{{ item.timeslot.time|slugify }}">{{ item.timeslot.time|date:"l, F j, Y" }}</a></li>
-                            {% endifchanged %}
-                        {% endfor %}
-                        <li>
-                            <hr/>
-                        </li>
-                        <li class="tz-display">Showing <span class="current-tz">{{ timezone }}</span> time</li>
-                        <li class="tz-display"><span> {# span avoids applying nav link styling to these shortcuts #}
-                            <a onclick="ietf_timezone.use('{{ timezone }}')">Meeting time</a> |
-                            <a onclick="ietf_timezone.use('local')">Local time</a> |
-                            <a onclick="ietf_timezone.use('UTC')">UTC</a></span>
-                        </li>
-                        {% if settings.DEBUG and settings.DEBUG_AGENDA %}
-                            <li>
-                                <hr/>
-                            </li>
-                            <li><span id="current-time"></span></li>
-                        {% endif %}
-                    </ul>
-                </div>
-                </div>
-
-            {% endcache %}
-
-            {# make the timezone available to JS #}
-            <span id="initial-data" hidden data-timezone="{{ timezone }}"></span>
-{% endblock %}
-
-{% block js %}
-    <script src="{% static 'ietf/js/moment.js' %}"></script>
-    <script src="{% static 'ietf/js/moment-timezone-with-data-10-year-range.js' %}"></script>
-    <script src="{% static 'ietf/js/timezone.js' %}"></script>
-    <script src="{% static 'ietf/js/agenda_timezone.js' %}"></script>
-    <script src="{% static 'ietf/js/agenda_filter.js' %}"></script>
-    <script src="{% static 'ietf/js/agenda/agenda_personalize.js' %}"></script>
-    <script>
-
-        {% if settings.DEBUG and settings.DEBUG_AGENDA %}
-            speedup = +$.urlParam('speedup')
-            if (speedup < 1) {
-                speedup = 1
-            }
-            start_time = moment().utc()
-            if ($.urlParam('date')) {
-                offset_time = moment.tz(decodeURIComponent($.urlParam('date')), 'UTC')
-            } else {
-                offset_time = start_time
-            }
-            if (speedup > 1 || offset_time != start_time) {
-                moment.now = function () {
-                    return (+new Date() - start_time) * speedup + offset_time
-                }
-            }
-        {% else %}
-            speedup = 1
-        {% endif %}
-
-        /* pull this from the agenda_personalize js module to make available to agenda_timezone */
-        meeting_timezone = agenda_personalize.meeting_timezone;
-    </script>
-{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/meeting/agenda_personalize_buttonlist.html b/ietf/templates/meeting/agenda_personalize_buttonlist.html
index fcad3a41e..99aee38a8 100644
--- a/ietf/templates/meeting/agenda_personalize_buttonlist.html
+++ b/ietf/templates/meeting/agenda_personalize_buttonlist.html
@@ -4,17 +4,17 @@ Buttons for the agenda_personalize.html template
 Required parameter: meeting - meeting being displayed
 {% endcomment %}
 {% load agenda_custom_tags %}
-<div class="buttonlist">
-    <a class="btn btn-primary agenda-link filterable"
-        href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
-        View customized agenda
-    </a>
-    <a class="btn btn-primary agenda-link filterable"
-        href="{% url 'ietf.meeting.views.agenda_ical' num=meeting.number %}">
-        Download as .ics
-    </a>
-    <a class="btn btn-primary agenda-link filterable"
+
+<div class="mb-3">
+    <a class="btn btn-sm btn-outline-primary visually-hidden ical-link agenda-link filterable"
         href="{% webcal_url 'ietf.meeting.views.agenda_ical' num=meeting.number %}">
-        Subscribe with webcal
+        Subscribe to personal agenda
     </a>
-</div>
+
+    <a class="visually-hidden btn btn-sm btn-outline-primary ical-link agenda-link filterable" href="{% url "ietf.meeting.views.agenda_ical" num=meeting.number %}">Download .ics of personal agenda</a>
+
+    <a class="btn btn-sm btn-outline-primary visually-hidden ical-link agenda-link filterable"
+        href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
+        View personal agenda
+    </a>
+</div>
\ No newline at end of file
diff --git a/ietf/templates/meeting/meeting_heading.html b/ietf/templates/meeting/meeting_heading.html
index 54603b7e9..c53a2f91b 100644
--- a/ietf/templates/meeting/meeting_heading.html
+++ b/ietf/templates/meeting/meeting_heading.html
@@ -13,6 +13,9 @@
 
 <h1>
     IETF {{ meeting.number }} Meeting Agenda {{ title_extra }}
+    {% if personalize %}
+        Personalization
+    {% endif %}
 </h1>
 <h4>
     {{ meeting.city|default:"Location TBD" }}, {{ meeting.date|date:"F j" }} -
@@ -42,6 +45,12 @@
                 UTC Agenda
             </a>
         </li>
+        <li class="nav-item">
+            <a class="nav-link agenda-link filterable {% if selected == "select-sessions" %}active{% endif %}" href="{% url 'ietf.meeting.views.agenda_personalize' num=meeting.number %}"
+                >
+                Personalize Agenda
+            </a>
+        </li>
         {% if user|has_role:"Secretariat,Area Director,IAB" %}
             {% if schedule != meeting.schedule %}
                 <li class="nav-item">
@@ -72,11 +81,5 @@
                 Plaintext
             </a>
         </li>
-        <li class="nav-item">
-            <a class="nav-link agenda-link filterable {% if selected == "select-sessions" %}active{% endif %}" href="{% url 'ietf.meeting.views.agenda_personalize' num=meeting.number %}"
-                >
-                Select Sessions
-            </a>
-        </li>
     </ul>
 </p>
\ No newline at end of file
diff --git a/package.json b/package.json
index 250cd4249..23c3d902e 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
     "ietf/static/js/agenda_filter.js",
     "ietf/static/js/agenda_materials.js",
     "ietf/static/js/agenda_timezone.js",
+    "ietf/static/js/agenda_personalize.js",
     "ietf/static/js/timezone.js",
     "ietf/static/js/room_params.js",
     "ietf/static/js/week-view.js",