This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new a88f971  Group JS files by origin
a88f971 is described below

commit a88f971d0684a91453717dc6f3fba44aba367525
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Dec 10 19:38:12 2025 +0000

    Group JS files by origin
---
 .pre-commit-config.yaml                            |   2 +
 .pre-commit-heavy.yaml                             |   6 +-
 Makefile                                           |   2 +-
 atr/admin/templates/update-keys.html               |   2 +-
 atr/admin/templates/update-projects.html           |   2 +-
 atr/get/finish.py                                  |   2 +-
 atr/get/tokens.py                                  |   2 +-
 atr/static/js/admin-update-form.js                 |  55 -------
 atr/static/js/announce-preview.js                  | 100 ------------
 atr/static/js/clipboard-copy.js                    |  32 ----
 atr/static/js/committee-directory.js               | 146 -----------------
 atr/static/js/copy-variable.js                     |  18 ---
 atr/static/js/create-a-jwt.js                      |  33 ----
 atr/static/js/create-a-jwt.js.map                  |   1 -
 atr/static/js/finish-selected-move.js.map          |   1 -
 atr/static/js/ignore-form-change.js                |  15 --
 atr/static/js/keys-add-toggle.js                   |  22 ---
 atr/static/js/{ => min}/bootstrap.bundle.min.js    |   0
 .../js/{ => min}/bootstrap.bundle.min.js.map       |   0
 atr/static/js/ongoing-tasks-poll.js                | 106 -------------
 atr/static/js/projects-add-form.js                 |  14 --
 atr/static/js/projects-directory.js                |  72 ---------
 atr/static/js/report-results.js                    | 100 ------------
 atr/static/js/src/admin-update-form.js             |  59 +++++++
 atr/static/js/src/announce-preview.js              | 107 +++++++++++++
 atr/static/js/src/clipboard-copy.js                |  34 ++++
 atr/static/js/src/committee-directory.js           | 166 +++++++++++++++++++
 atr/static/js/src/copy-variable.js                 |  18 +++
 atr/static/js/src/ignore-form-change.js            |  17 ++
 atr/static/js/src/keys-add-toggle.js               |  26 +++
 atr/static/js/src/ongoing-tasks-poll.js            | 119 ++++++++++++++
 atr/static/js/src/projects-add-form.js             |  20 +++
 atr/static/js/src/projects-directory.js            |  74 +++++++++
 atr/static/js/src/report-results.js                | 105 ++++++++++++
 atr/static/js/src/vote-preview.js                  |  74 +++++++++
 atr/static/js/ts/create-a-jwt.js                   |  23 +++
 atr/static/js/ts/create-a-jwt.js.map               |   1 +
 atr/static/js/{ => ts}/finish-selected-move.js     | 176 ++++++++++-----------
 atr/static/js/ts/finish-selected-move.js.map       |   1 +
 atr/static/js/vote-preview.js                      |  70 --------
 atr/template.py                                    |   6 +-
 atr/templates/check-selected.html                  |   2 +-
 atr/templates/committee-directory.html             |   2 +-
 atr/templates/layouts/base.html                    |   2 +-
 atr/templates/projects.html                        |   2 +-
 atr/templates/report-selected-path.html            |   2 +-
 bootstrap/make.sh                                  |   4 +-
 tsconfig.json                                      |   6 +-
 48 files changed, 948 insertions(+), 901 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fc00d59..4b3ddcc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,10 +20,12 @@ repos:
     - id: check-yaml
     - id: detect-private-key
     - id: end-of-file-fixer
+      exclude: ^atr/static/js/(ts|min)/
     - id: fix-byte-order-marker
     - id: forbid-submodules
     - id: mixed-line-ending
     - id: trailing-whitespace
+      exclude: ^atr/static/js/(ts|min)/
 - repo: https://github.com/Lucas-C/pre-commit-hooks
   rev: v1.5.5
   hooks:
diff --git a/.pre-commit-heavy.yaml b/.pre-commit-heavy.yaml
index 5d0ad8b..5cf9eac 100644
--- a/.pre-commit-heavy.yaml
+++ b/.pre-commit-heavy.yaml
@@ -7,16 +7,14 @@ repos:
       entry: biome check --write --files-ignore-unknown=true 
--no-errors-on-unmatched --colors=off
       language: system
       types: [javascript]
-      files: ^atr/static/js/.*\.js$
-      exclude: (.*\.min\.js$|.*\.ts\.js$)
+      files: ^atr/static/js/src/.*\.js$
 - repo: https://github.com/oxc-project/mirrors-oxlint
   rev: v1.32.0
   hooks:
     - id: oxlint
       name: oxlint JS linter
       types: [javascript]
-      files: "atr/static/js/.*\\.js$"
-      exclude: (.*\.min\.js$|.*\.ts\.js$)
+      files: ^atr/static/js/src/.*\.js$
       args:
         - --deny=correctness
         - --deny=perf
diff --git a/Makefile b/Makefile
index 3895f78..b57090b 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ build-playwright:
        docker build -t atr-playwright -f tests/Dockerfile.playwright playwright
 
 build-ts:
-       tsc -p tsconfig.json
+       tsgo --project ./tsconfig.json
 
 build-ubuntu:
        scripts/build Dockerfile.ubuntu $(IMAGE)
diff --git a/atr/admin/templates/update-keys.html 
b/atr/admin/templates/update-keys.html
index 8319ba7..78f3c5f 100644
--- a/atr/admin/templates/update-keys.html
+++ b/atr/admin/templates/update-keys.html
@@ -91,5 +91,5 @@
 
 {% block javascripts %}
     {{ super() }}
-    <script src="{{ static_url('js/admin-update-form.js') }}"></script>
+    <script src="{{ static_url('js/src/admin-update-form.js') }}"></script>
 {% endblock javascripts %}
diff --git a/atr/admin/templates/update-projects.html 
b/atr/admin/templates/update-projects.html
index f582e9c..033e4d6 100644
--- a/atr/admin/templates/update-projects.html
+++ b/atr/admin/templates/update-projects.html
@@ -91,5 +91,5 @@
 
 {% block javascripts %}
     {{ super() }}
-    <script src="{{ static_url('js/admin-update-form.js') }}"></script>
+    <script src="{{ static_url('js/src/admin-update-form.js') }}"></script>
 {% endblock javascripts %}
diff --git a/atr/get/finish.py b/atr/get/finish.py
index 63f0940..63731a8 100644
--- a/atr/get/finish.py
+++ b/atr/get/finish.py
@@ -330,7 +330,7 @@ async def _render_page(
     page.append(
         htpy.script(
             id="main-script-data",
-            src=util.static_url("js/finish-selected-move.js"),
+            src=util.static_url("js/ts/finish-selected-move.js"),
             **{"data-csrf-token": csrf_token},
         )[""]
     )
diff --git a/atr/get/tokens.py b/atr/get/tokens.py
index f9a11af..0f64732 100644
--- a/atr/get/tokens.py
+++ b/atr/get/tokens.py
@@ -82,7 +82,7 @@ async def tokens(session: web.Committer) -> str:
         title="Tokens",
         description="Manage your PATs and JWTs.",
         content=page.collect(),
-        javascripts=["create-a-jwt"],
+        typescripts=["create-a-jwt"],
     )
 
 
diff --git a/atr/static/js/admin-update-form.js 
b/atr/static/js/admin-update-form.js
deleted file mode 100644
index f73ff88..0000000
--- a/atr/static/js/admin-update-form.js
+++ /dev/null
@@ -1,55 +0,0 @@
-document.addEventListener("DOMContentLoaded", () => {
-    const form = document.querySelector("form");
-    const button = form.querySelector("button[type='submit']");
-
-    form.addEventListener("submit", async (e) => {
-        e.preventDefault();
-
-        button.disabled = true;
-        document.body.style.cursor = "wait";
-
-        const statusElement = document.getElementById("status");
-        while (statusElement.firstChild) {
-            statusElement.firstChild.remove();
-        }
-
-        const csrfToken = 
document.querySelector("input[name='csrf_token']").value;
-
-        try {
-            const response = await fetch(window.location.href, {
-                method: "POST",
-                headers: {
-                    "X-CSRFToken": csrfToken
-                }
-            });
-
-            if (!response.ok) {
-                addStatusMessage(statusElement, "Could not make network 
request", "error");
-                return;
-            }
-
-            const data = await response.json();
-            addStatusMessage(statusElement, data.message, data.category);
-        } catch (error) {
-            addStatusMessage(statusElement, error, "error");
-        } finally {
-            button.disabled = false;
-            document.body.style.cursor = "default";
-        }
-    });
-});
-
-function addStatusMessage(parentElement, message, category) {
-    const divElement = document.createElement("div");
-    divElement.classList.add("page-status-message");
-    divElement.classList.add(category);
-    if (category === "error") {
-        const prefixElement = document.createElement("strong");
-        const textElement = document.createTextNode("Error: ");
-        prefixElement.appendChild(textElement);
-        divElement.appendChild(prefixElement);
-    }
-    const textNode = document.createTextNode(message);
-    divElement.appendChild(textNode);
-    parentElement.appendChild(divElement);
-}
diff --git a/atr/static/js/announce-preview.js 
b/atr/static/js/announce-preview.js
deleted file mode 100644
index 1833864..0000000
--- a/atr/static/js/announce-preview.js
+++ /dev/null
@@ -1,100 +0,0 @@
-document.addEventListener("DOMContentLoaded", () => {
-    let debounceTimeout;
-    const debounceDelay = 500;
-
-    const bodyTextarea = document.getElementById("body");
-    const textPreviewContent = 
document.getElementById("announce-body-preview-content");
-    const announceForm = document.querySelector("form.atr-canary");
-    const configElement = document.getElementById("announce-config");
-
-    if (!bodyTextarea || !textPreviewContent || !announceForm) {
-        console.error("Required elements for announce preview not found. 
Exiting.");
-        return;
-    }
-
-    const previewUrl = configElement ? configElement.dataset.previewUrl : null;
-    const csrfTokenInput = 
announceForm.querySelector('input[name="csrf_token"]');
-
-    if (!previewUrl || !csrfTokenInput) {
-        console.error("Required data attributes or CSRF token not found for 
announce preview.");
-        return;
-    }
-    const csrfToken = csrfTokenInput.value;
-
-    function fetchAndUpdateAnnouncePreview() {
-        const bodyContent = bodyTextarea.value;
-
-        fetch(previewUrl, {
-                method: "POST",
-                headers: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "X-CSRFToken": csrfToken
-                },
-                body: new URLSearchParams({
-                    "body": bodyContent,
-                    "csrf_token": csrfToken
-                })
-            })
-            .then(response => {
-                if (!response.ok) {
-                    return response.text().then(text => {
-                        throw new Error(`HTTP error ${response.status}: 
${text}`)
-                    });
-                }
-                return response.text();
-            })
-            .then(previewText => {
-                textPreviewContent.textContent = previewText;
-            })
-            .catch(error => {
-                console.error("Error fetching email preview:", error);
-                textPreviewContent.textContent = `Error loading 
preview:\n${error.message}`;
-            });
-    }
-
-    bodyTextarea.addEventListener("input", () => {
-        clearTimeout(debounceTimeout);
-        debounceTimeout = setTimeout(fetchAndUpdateAnnouncePreview, 
debounceDelay);
-    });
-
-    fetchAndUpdateAnnouncePreview();
-
-    // Download path suffix validation
-    const pathInput = document.getElementById("download_path_suffix");
-    const pathHelpText = pathInput ? 
pathInput.parentElement.querySelector(".form-text") : null;
-
-    if (pathInput && pathHelpText) {
-        const baseText = pathHelpText.dataset.baseText || "";
-        let pathDebounce;
-
-        function updatePathHelpText() {
-            let suffix = pathInput.value;
-            if (suffix.includes("..") || suffix.includes("//")) {
-                pathHelpText.textContent = "Download path suffix must not 
contain .. or //";
-                return;
-            }
-            if (suffix.startsWith("./")) {
-                suffix = suffix.substring(1);
-            } else if (suffix === ".") {
-                suffix = "/";
-            }
-            if (!suffix.startsWith("/")) {
-                suffix = "/" + suffix;
-            }
-            if (!suffix.endsWith("/")) {
-                suffix = suffix + "/";
-            }
-            if (suffix.includes("/.")) {
-                pathHelpText.textContent = "Download path suffix must not 
contain /.";
-                return;
-            }
-            pathHelpText.textContent = baseText + suffix;
-        }
-
-        pathInput.addEventListener("input", () => {
-            clearTimeout(pathDebounce);
-            pathDebounce = setTimeout(updatePathHelpText, 10);
-        });
-        updatePathHelpText();
-    }
-});
diff --git a/atr/static/js/clipboard-copy.js b/atr/static/js/clipboard-copy.js
deleted file mode 100644
index b15eccc..0000000
--- a/atr/static/js/clipboard-copy.js
+++ /dev/null
@@ -1,32 +0,0 @@
-document.addEventListener("DOMContentLoaded", function () {
-    const copyButtons = document.querySelectorAll(".atr-copy-btn");
-
-    copyButtons.forEach(button => {
-        button.addEventListener("click", function () {
-            const targetId = this.getAttribute("data-clipboard-target");
-            const targetElement = document.querySelector(targetId);
-
-            if (targetElement) {
-                const textToCopy = targetElement.textContent;
-
-                navigator.clipboard.writeText(textToCopy)
-                    .then(() => {
-                        const originalText = this.innerHTML;
-                        this.innerHTML = '<i class="bi bi-check"></i> Copied!';
-
-                        setTimeout(() => {
-                            this.innerHTML = originalText;
-                        }, 2000);
-                    })
-                    .catch(err => {
-                        console.error("Failed to copy: ", err);
-                        this.innerHTML = '<i class="bi 
bi-exclamation-triangle"></i> Failed!';
-
-                        setTimeout(() => {
-                            this.innerHTML = '<i class="bi bi-clipboard"></i> 
Copy';
-                        }, 2000);
-                    });
-            }
-        });
-    });
-});
diff --git a/atr/static/js/committee-directory.js 
b/atr/static/js/committee-directory.js
deleted file mode 100644
index 3e02fbf..0000000
--- a/atr/static/js/committee-directory.js
+++ /dev/null
@@ -1,146 +0,0 @@
-let allCommitteeCards = [];
-
-function filterCommitteesByText() {
-    const projectFilter = document.getElementById("project-filter").value;
-    const cards = allCommitteeCards;
-    let visibleCount = 0;
-
-    if (participantButton && participantButton.dataset.showing === 
"participant") {
-        participantButton.dataset.showing = "all";
-        participantButton.textContent = "Show my committees";
-        participantButton.setAttribute("aria-pressed", "false");
-    }
-
-    for (let card of cards) {
-        const nameElement = card.querySelector(".card-title");
-        const name = nameElement.textContent.trim();
-        if (!projectFilter) {
-            card.parentElement.hidden = false;
-            visibleCount++;
-        } else {
-            let regex;
-            try {
-                regex = new RegExp(projectFilter, "i");
-            } catch (e) {
-                const escapedFilter = 
projectFilter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-                regex = new RegExp(escapedFilter, "i");
-            }
-            card.parentElement.hidden = !name.match(regex);
-            if (!card.parentElement.hidden) {
-                visibleCount++;
-            }
-        }
-    }
-    document.getElementById("committee-count").textContent = visibleCount;
-}
-
-document.getElementById("filter-button").addEventListener("click", 
filterCommitteesByText);
-document.getElementById("project-filter").addEventListener("keydown", 
function(event) {
-    if (event.key === "Enter") {
-        filterCommitteesByText();
-        event.preventDefault();
-    }
-});
-
-const participantButton = document.getElementById("filter-participant-button");
-if (participantButton) {
-    participantButton.addEventListener("click", function() {
-        const showing = this.dataset.showing;
-        const cards = allCommitteeCards;
-        let visibleCount = 0;
-
-        if (showing === "all") {
-            cards.forEach(card => {
-                const isParticipant = card.dataset.isParticipant === "true";
-                card.parentElement.hidden = !isParticipant;
-                if (!card.parentElement.hidden) {
-                    visibleCount++;
-                }
-            });
-            this.textContent = "Show all committees";
-            this.dataset.showing = "participant";
-            this.setAttribute("aria-pressed", "true");
-        } else {
-            cards.forEach(card => {
-                card.parentElement.hidden = false;
-                visibleCount++;
-            });
-            this.textContent = "Show my committees";
-            this.dataset.showing = "all";
-            this.setAttribute("aria-pressed", "false");
-        }
-        document.getElementById("project-filter").value = "";
-        document.getElementById("committee-count").textContent = visibleCount;
-    });
-}
-
-document.addEventListener("DOMContentLoaded", function() {
-    // Hide images that fail to load
-    document.querySelectorAll(".page-logo").forEach(function(img) {
-        img.addEventListener("error", function() {
-            this.style.display = "none";
-        });
-    });
-
-    allCommitteeCards = 
Array.from(document.querySelectorAll(".page-project-card"));
-    const cards = allCommitteeCards;
-    const committeeCountSpan = document.getElementById("committee-count");
-    let initialVisibleCount = 0;
-    const initialShowingMode = participantButton ? 
participantButton.dataset.showing : "all";
-
-    if (participantButton) {
-        if (initialShowingMode === "participant") {
-            participantButton.setAttribute("aria-pressed", "true");
-        } else {
-            participantButton.setAttribute("aria-pressed", "false");
-        }
-    }
-
-    if (initialShowingMode === "participant") {
-        cards.forEach(card => {
-            const isParticipant = card.dataset.isParticipant === "true";
-            card.parentElement.hidden = !isParticipant;
-            if (!card.parentElement.hidden) {
-                initialVisibleCount++;
-            }
-        });
-    } else {
-        cards.forEach(card => {
-            card.parentElement.hidden = false;
-            initialVisibleCount++;
-        });
-    }
-    committeeCountSpan.textContent = initialVisibleCount;
-
-    // Add a click listener to project subcards to handle navigation
-    // TODO: Improve accessibility
-    
document.querySelectorAll(".page-project-subcard").forEach(function(subcard) {
-        subcard.addEventListener("click", function(event) {
-            if (this.dataset.projectUrl) {
-                window.location.href = this.dataset.projectUrl;
-            }
-        });
-    });
-
-    // Add a click listener for toggling project visibility within each 
committee
-    
document.querySelectorAll(".page-toggle-committee-projects").forEach(function(button)
 {
-        button.addEventListener("click", function() {
-            const projectListContainer = 
this.closest(".page-project-list-container");
-            if (projectListContainer) {
-                const extraProjects = 
projectListContainer.querySelectorAll(".page-project-extra");
-                extraProjects.forEach(function(proj) {
-                    proj.classList.toggle("d-none");
-                });
-
-                const isExpanded = this.getAttribute("aria-expanded") === 
"true";
-                if (isExpanded) {
-                    this.textContent = this.dataset.textShow;
-                    this.setAttribute("aria-expanded", "false");
-                } else {
-                    this.textContent = this.dataset.textHide;
-                    this.setAttribute("aria-expanded", "true");
-                }
-            }
-        });
-    });
-});
diff --git a/atr/static/js/copy-variable.js b/atr/static/js/copy-variable.js
deleted file mode 100644
index 2b81d51..0000000
--- a/atr/static/js/copy-variable.js
+++ /dev/null
@@ -1,18 +0,0 @@
-document.addEventListener("DOMContentLoaded", function () {
-    document.querySelectorAll(".copy-var-btn").forEach(btn => {
-        btn.addEventListener("click", () => {
-            const variable = btn.dataset.variable;
-            navigator.clipboard.writeText(variable).then(() => {
-                const originalText = btn.textContent;
-                btn.textContent = "Copied!";
-                btn.classList.remove("btn-outline-secondary");
-                btn.classList.add("btn-success");
-                setTimeout(() => {
-                    btn.textContent = originalText;
-                    btn.classList.remove("btn-success");
-                    btn.classList.add("btn-outline-secondary");
-                }, 1500);
-            });
-        });
-    });
-});
diff --git a/atr/static/js/create-a-jwt.js b/atr/static/js/create-a-jwt.js
deleted file mode 100644
index e9a56e0..0000000
--- a/atr/static/js/create-a-jwt.js
+++ /dev/null
@@ -1,33 +0,0 @@
-"use strict";
-var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, 
generator) {
-    function adopt(value) { return value instanceof P ? value : new P(function 
(resolve) { resolve(value); }); }
-    return new (P || (P = Promise))(function (resolve, reject) {
-        function fulfilled(value) { try { step(generator.next(value)); } catch 
(e) { reject(e); } }
-        function rejected(value) { try { step(generator["throw"](value)); } 
catch (e) { reject(e); } }
-        function step(result) { result.done ? resolve(result.value) : 
adopt(result.value).then(fulfilled, rejected); }
-        step((generator = generator.apply(thisArg, _arguments || [])).next());
-    });
-};
-document.addEventListener("DOMContentLoaded", () => {
-    const form = document.getElementById("issue-jwt-form");
-    const output = document.getElementById("jwt-output");
-    if (!form || !output) {
-        return;
-    }
-    form.addEventListener("submit", (e) => __awaiter(void 0, void 0, void 0, 
function* () {
-        e.preventDefault();
-        const resp = yield fetch(form.action, {
-            method: "POST",
-            body: new FormData(form),
-        });
-        if (resp.ok) {
-            const token = yield resp.text();
-            output.classList.remove("d-none");
-            output.textContent = token;
-        }
-        else {
-            alert("Failed to fetch JWT");
-        }
-    }));
-});
-//# sourceMappingURL=create-a-jwt.js.map
diff --git a/atr/static/js/create-a-jwt.js.map 
b/atr/static/js/create-a-jwt.js.map
deleted file mode 100644
index 5288535..0000000
--- a/atr/static/js/create-a-jwt.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"create-a-jwt.js","sourceRoot":"","sources":["../ts/create-a-jwt.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAS,EAAE;IACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAA2B,CAAC;IACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAErD,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IAED,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAO,CAAQ,EAAiB,EAAE;QAChE,CAAC,CAAC,cAAc,EAAE,CAAC;QAEnB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAA
 [...]
diff --git a/atr/static/js/finish-selected-move.js.map 
b/atr/static/js/finish-selected-move.js.map
deleted file mode 100644
index 4c7c9c4..0000000
--- a/atr/static/js/finish-selected-move.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,
 [...]
diff --git a/atr/static/js/ignore-form-change.js 
b/atr/static/js/ignore-form-change.js
deleted file mode 100644
index a852346..0000000
--- a/atr/static/js/ignore-form-change.js
+++ /dev/null
@@ -1,15 +0,0 @@
-document.addEventListener("DOMContentLoaded", function () {
-    document.querySelectorAll("table.page-details 
input.form-control").forEach(function (input) {
-        var row = input.closest("tr");
-        var updateBtn = row.querySelector("button.btn-primary");
-        function check() {
-            if (input.value !== input.dataset.value) {
-                updateBtn.classList.remove("disabled");
-            } else {
-                updateBtn.classList.add("disabled");
-            }
-        }
-        input.addEventListener("input", check);
-        check();
-    });
-});
diff --git a/atr/static/js/keys-add-toggle.js b/atr/static/js/keys-add-toggle.js
deleted file mode 100644
index 8ac378d..0000000
--- a/atr/static/js/keys-add-toggle.js
+++ /dev/null
@@ -1,22 +0,0 @@
-document.addEventListener("DOMContentLoaded", function() {
-    const checkboxes = 
document.querySelectorAll("input[name='selected_committees']");
-    if (checkboxes.length === 0) return;
-
-    const firstCheckbox = checkboxes[0];
-    const container = firstCheckbox.closest(".col-sm-8");
-    if (!container) return;
-
-    const button = document.createElement("button");
-    button.id = "toggleCommitteesBtn";
-    button.type = "button";
-    button.className = "btn btn-outline-secondary btn-sm mt-2";
-    button.textContent = "Select all committees";
-
-    button.addEventListener("click", function() {
-        const allChecked = Array.from(checkboxes).every(cb => cb.checked);
-        checkboxes.forEach(cb => cb.checked = !allChecked);
-        button.textContent = allChecked ? "Select all committees" : "Deselect 
all committees";
-    });
-
-    container.appendChild(button);
-});
diff --git a/atr/static/js/bootstrap.bundle.min.js 
b/atr/static/js/min/bootstrap.bundle.min.js
similarity index 100%
rename from atr/static/js/bootstrap.bundle.min.js
rename to atr/static/js/min/bootstrap.bundle.min.js
diff --git a/atr/static/js/bootstrap.bundle.min.js.map 
b/atr/static/js/min/bootstrap.bundle.min.js.map
similarity index 100%
rename from atr/static/js/bootstrap.bundle.min.js.map
rename to atr/static/js/min/bootstrap.bundle.min.js.map
diff --git a/atr/static/js/ongoing-tasks-poll.js 
b/atr/static/js/ongoing-tasks-poll.js
deleted file mode 100644
index 33c08dc..0000000
--- a/atr/static/js/ongoing-tasks-poll.js
+++ /dev/null
@@ -1,106 +0,0 @@
-(function() {
-    // Handle More and Less toggle buttons for collapse sections
-    
document.querySelectorAll(".page-collapse-toggle").forEach(function(button) {
-        button.addEventListener("click", function() {
-            this.textContent = this.textContent.trim() === "More" ? "Less" : 
"More";
-        });
-    });
-
-    const banner = document.getElementById("ongoing-tasks-banner");
-    if (!banner) return;
-
-    const apiUrl = banner.dataset.apiUrl;
-    if (!apiUrl) return;
-
-    const countSpan = document.getElementById("ongoing-tasks-count");
-    const textSpan = document.getElementById("ongoing-tasks-text");
-    const voteButton = document.getElementById("start-vote-button");
-    const progress = document.getElementById("poll-progress");
-    const pollInterval = 3000;
-
-    let currentCount = parseInt(countSpan?.textContent || "0", 10);
-    if (currentCount === 0) return;
-
-    function restartProgress() {
-        if (!progress) return;
-        progress.style.animation = "none";
-        progress.offsetHeight;
-        progress.style.animation = `poll-grow ${pollInterval}ms linear 
forwards`;
-    }
-
-    function updateBanner(count) {
-        if (!countSpan || !textSpan) return;
-
-        currentCount = count;
-        countSpan.textContent = count;
-
-        const taskWord = count === 1 ? "task" : "tasks";
-        const isAre = count === 1 ? "is" : "are";
-        // TODO: Migrate away from setting innerHTML
-        textSpan.innerHTML = `There ${isAre} currently <strong 
id="ongoing-tasks-count">${count}</strong> background verification ${taskWord} 
running for the latest revision. Results shown below may be incomplete or 
outdated until the tasks finish.`;
-
-        if (count === 0) {
-            // Banner always exists, but we hide it
-            banner.classList.add("d-none");
-            enableVoteButton();
-        }
-    }
-
-    function enableVoteButton() {
-        if (!voteButton) return;
-        if (!voteButton.classList.contains("disabled")) return;
-
-        const voteHref = voteButton.dataset.voteHref || 
voteButton.getAttribute("href");
-        if (!voteHref || voteHref === "#") return;
-
-        voteButton.classList.remove("disabled");
-        voteButton.removeAttribute("aria-disabled");
-        voteButton.removeAttribute("tabindex");
-        voteButton.removeAttribute("role");
-        voteButton.setAttribute("href", voteHref);
-        voteButton.setAttribute("title", "Start a vote on this draft");
-    }
-
-    function pollOngoingTasks() {
-        if (currentCount === 0) return;
-
-        if (progress) {
-            progress.style.animation = "none";
-            progress.style.width = "100%";
-            progress.classList.remove("bg-warning");
-            progress.classList.add("bg-info", "progress-bar-striped", 
"progress-bar-animated");
-        }
-        fetch(apiUrl)
-            .then(response => {
-                if (!response.ok) throw new Error(`HTTP ${response.status}`);
-                return response.json();
-            })
-            .then(data => {
-                if (progress) {
-                    progress.classList.remove("bg-info", 
"progress-bar-striped", "progress-bar-animated");
-                    progress.classList.add("bg-warning");
-                }
-                const newCount = data.ongoing || 0;
-                if (newCount !== currentCount) {
-                    updateBanner(newCount);
-                }
-                if (newCount > 0) {
-                    restartProgress();
-                    setTimeout(pollOngoingTasks, pollInterval);
-                }
-            })
-            .catch(error => {
-                console.error("Error polling ongoing tasks:", error);
-                if (progress) {
-                    progress.classList.remove("bg-info", 
"progress-bar-striped", "progress-bar-animated");
-                    progress.classList.add("bg-warning");
-                }
-                restartProgress();
-                // Double the interval when there's an error
-                setTimeout(pollOngoingTasks, pollInterval * 2);
-            });
-    }
-
-    restartProgress();
-    setTimeout(pollOngoingTasks, pollInterval);
-})();
diff --git a/atr/static/js/projects-add-form.js 
b/atr/static/js/projects-add-form.js
deleted file mode 100644
index aff9e77..0000000
--- a/atr/static/js/projects-add-form.js
+++ /dev/null
@@ -1,14 +0,0 @@
-document.addEventListener("DOMContentLoaded", function() {
-    const configElement = document.getElementById("projects-add-config");
-    if (!configElement) return;
-
-    const committeeDisplayName = configElement.dataset.committeeDisplayName;
-    const committeeName = configElement.dataset.committeeName;
-    if (!committeeDisplayName || !committeeName) return;
-
-    const formTexts = document.querySelectorAll(".form-text, .text-muted");
-    formTexts.forEach(function(element) {
-        element.textContent = element.textContent.replace(/Example/g, 
committeeDisplayName);
-        element.textContent = element.textContent.replace(/example/g, 
committeeName.toLowerCase());
-    });
-});
diff --git a/atr/static/js/projects-directory.js 
b/atr/static/js/projects-directory.js
deleted file mode 100644
index 705cfcd..0000000
--- a/atr/static/js/projects-directory.js
+++ /dev/null
@@ -1,72 +0,0 @@
-function filter() {
-    const projectFilter = document.getElementById("project-filter").value;
-    const cards = document.querySelectorAll(".page-project-card");
-    let visibleCount = 0;
-    for (let card of cards) {
-        const nameElement = card.querySelector(".card-title");
-        const name = nameElement.innerHTML;
-        if (!projectFilter) {
-            card.parentElement.hidden = false;
-            visibleCount++;
-        } else {
-            card.parentElement.hidden = !name.match(new RegExp(projectFilter, 
"i"));
-            if (!card.parentElement.hidden) {
-                visibleCount++;
-            }
-        }
-    }
-    document.getElementById("project-count").textContent = visibleCount;
-}
-
-// Add event listeners
-document.getElementById("filter-button").addEventListener("click", filter);
-document.getElementById("project-filter").addEventListener("keydown", 
function(event) {
-    if (event.key === "Enter") {
-        filter();
-        event.preventDefault();
-    }
-});
-
-// Add click handlers for project cards
-document.querySelectorAll(".page-project-card").forEach(function(card) {
-    card.addEventListener("click", function(event) {
-        // Prevent card navigation if click is inside a form
-        if (event.target.closest("form")) {
-            return;
-        }
-        window.location.href = this.getAttribute("data-project-url");
-    });
-});
-
-// Participant filter logic
-const participantButton = document.getElementById("filter-participant-button");
-participantButton.addEventListener("click", function() {
-    const showing = this.dataset.showing;
-    const cards = document.querySelectorAll(".page-project-card");
-    let visibleCount = 0;
-
-    if (showing === "all") {
-        // Switch to showing only participant projects
-        cards.forEach(card => {
-            const isParticipant = card.dataset.isParticipant === "true";
-            card.parentElement.hidden = !isParticipant;
-            if (!card.parentElement.hidden) {
-                visibleCount++;
-            }
-        });
-        this.textContent = "Show all projects";
-        this.dataset.showing = "participant";
-    } else {
-        // Switch to showing all projects
-        cards.forEach(card => {
-            card.parentElement.hidden = false;
-            visibleCount++;
-        });
-        this.textContent = "Show my projects";
-        this.dataset.showing = "all";
-    }
-    // Reset text filter when toggling participant view
-    document.getElementById("project-filter").value = "";
-    // Update count
-    document.getElementById("project-count").textContent = visibleCount;
-});
diff --git a/atr/static/js/report-results.js b/atr/static/js/report-results.js
deleted file mode 100644
index 34dc44e..0000000
--- a/atr/static/js/report-results.js
+++ /dev/null
@@ -1,100 +0,0 @@
-function toggleAllDetails() {
-    const details = document.querySelectorAll("details");
-    // Check if any are closed
-    const anyClosed = Array.from(details).some(detail => !detail.open);
-    // If any are closed, open all
-    // Otherwise, close all
-    details.forEach(detail => detail.open = anyClosed);
-}
-
-function toggleStatusVisibility(type, status) {
-    const btn = document.getElementById(`btn-toggle-${type}-${status}`);
-    const targets = 
document.querySelectorAll(`.atr-result-${type}.atr-result-status-${status}`);
-    if (!targets.length) return;
-    let elementsCurrentlyHidden = targets[0].classList.contains("atr-hide");
-    targets.forEach(el => {
-        if (elementsCurrentlyHidden) {
-            el.classList.remove("atr-hide");
-        } else {
-            el.classList.add("atr-hide");
-        }
-    });
-    const bsSt = (status === "failure" || status === "exception") ? "danger" : 
status;
-    const cntMatch = btn.textContent.match(/\((\d+)\)/);
-    if (!cntMatch) {
-        console.error("Button text regex mismatch for:", btn.textContent);
-        return;
-    }
-    const cnt = cntMatch[0];
-    const newButtonAction = elementsCurrentlyHidden ? "Hide" : "Show";
-    btn.querySelector("span").textContent = newButtonAction;
-    if (newButtonAction === "Hide") {
-        btn.classList.remove(`btn-outline-${bsSt}`);
-        btn.classList.add(`btn-${bsSt}`);
-    } else {
-        btn.classList.remove(`btn-${bsSt}`);
-        btn.classList.add(`btn-outline-${bsSt}`);
-    }
-    if (type === "member") {
-        updateMemberStriping();
-    } else if (type === "primary") {
-        updatePrimaryStriping();
-    }
-}
-
-function restripeVisibleRows(rowSelector, stripeClass) {
-    let visibleIdx = 0;
-    document.querySelectorAll(rowSelector).forEach(row => {
-        row.classList.remove(stripeClass);
-        const hidden = row.classList.contains("atr-hide") || 
row.classList.contains("page-member-path-hide");
-        if (!hidden) {
-            if (visibleIdx % 2 === 0) row.classList.add(stripeClass);
-            visibleIdx++;
-        }
-    });
-}
-
-function updatePrimaryStriping() {
-    restripeVisibleRows(".atr-result-primary", "page-member-visible-odd");
-}
-
-function updateMemberStriping() {
-    restripeVisibleRows(".atr-result-member", "page-member-visible-odd");
-}
-
-// Toggle status visibility buttons
-document.querySelectorAll(".page-toggle-status").forEach(function(btn) {
-    btn.addEventListener("click", function() {
-        const type = this.dataset.type;
-        const status = this.dataset.status;
-        toggleStatusVisibility(type, status);
-    });
-});
-
-// Toggle all details button
-const toggleAllBtn = document.getElementById("btn-toggle-all-details");
-if (toggleAllBtn) {
-    toggleAllBtn.addEventListener("click", toggleAllDetails);
-}
-
-// Member path filter
-const mpfInput = document.getElementById("member-path-filter");
-if (mpfInput) {
-    mpfInput.addEventListener("input", function() {
-        const filterText = this.value.toLowerCase();
-        document.querySelectorAll(".atr-result-member").forEach(row => {
-            const pathCell = row.cells[0];
-            let hide = false;
-            if (filterText) {
-                if (!pathCell.textContent.toLowerCase().includes(filterText)) {
-                    hide = true;
-                }
-            }
-            row.classList.toggle("page-member-path-hide", hide);
-        });
-        updateMemberStriping();
-    });
-}
-
-updatePrimaryStriping();
-updateMemberStriping();
diff --git a/atr/static/js/src/admin-update-form.js 
b/atr/static/js/src/admin-update-form.js
new file mode 100644
index 0000000..e7a9b7c
--- /dev/null
+++ b/atr/static/js/src/admin-update-form.js
@@ -0,0 +1,59 @@
+document.addEventListener("DOMContentLoaded", () => {
+       const form = document.querySelector("form");
+       const button = form.querySelector("button[type='submit']");
+
+       form.addEventListener("submit", async (e) => {
+               e.preventDefault();
+
+               button.disabled = true;
+               document.body.style.cursor = "wait";
+
+               const statusElement = document.getElementById("status");
+               while (statusElement.firstChild) {
+                       statusElement.firstChild.remove();
+               }
+
+               const csrfToken = 
document.querySelector("input[name='csrf_token']").value;
+
+               try {
+                       const response = await fetch(window.location.href, {
+                               method: "POST",
+                               headers: {
+                                       "X-CSRFToken": csrfToken,
+                               },
+                       });
+
+                       if (!response.ok) {
+                               addStatusMessage(
+                                       statusElement,
+                                       "Could not make network request",
+                                       "error",
+                               );
+                               return;
+                       }
+
+                       const data = await response.json();
+                       addStatusMessage(statusElement, data.message, 
data.category);
+               } catch (error) {
+                       addStatusMessage(statusElement, error, "error");
+               } finally {
+                       button.disabled = false;
+                       document.body.style.cursor = "default";
+               }
+       });
+});
+
+function addStatusMessage(parentElement, message, category) {
+       const divElement = document.createElement("div");
+       divElement.classList.add("page-status-message");
+       divElement.classList.add(category);
+       if (category === "error") {
+               const prefixElement = document.createElement("strong");
+               const textElement = document.createTextNode("Error: ");
+               prefixElement.appendChild(textElement);
+               divElement.appendChild(prefixElement);
+       }
+       const textNode = document.createTextNode(message);
+       divElement.appendChild(textNode);
+       parentElement.appendChild(divElement);
+}
diff --git a/atr/static/js/src/announce-preview.js 
b/atr/static/js/src/announce-preview.js
new file mode 100644
index 0000000..ea387cb
--- /dev/null
+++ b/atr/static/js/src/announce-preview.js
@@ -0,0 +1,107 @@
+document.addEventListener("DOMContentLoaded", () => {
+       let debounceTimeout;
+       const debounceDelay = 500;
+
+       const bodyTextarea = document.getElementById("body");
+       const textPreviewContent = document.getElementById(
+               "announce-body-preview-content",
+       );
+       const announceForm = document.querySelector("form.atr-canary");
+       const configElement = document.getElementById("announce-config");
+
+       if (!bodyTextarea || !textPreviewContent || !announceForm) {
+               console.error("Required elements for announce preview not 
found. Exiting.");
+               return;
+       }
+
+       const previewUrl = configElement ? configElement.dataset.previewUrl : 
null;
+       const csrfTokenInput = 
announceForm.querySelector('input[name="csrf_token"]');
+
+       if (!previewUrl || !csrfTokenInput) {
+               console.error(
+                       "Required data attributes or CSRF token not found for 
announce preview.",
+               );
+               return;
+       }
+       const csrfToken = csrfTokenInput.value;
+
+       function fetchAndUpdateAnnouncePreview() {
+               const bodyContent = bodyTextarea.value;
+
+               fetch(previewUrl, {
+                       method: "POST",
+                       headers: {
+                               "Content-Type": 
"application/x-www-form-urlencoded",
+                               "X-CSRFToken": csrfToken,
+                       },
+                       body: new URLSearchParams({
+                               body: bodyContent,
+                               csrf_token: csrfToken,
+                       }),
+               })
+                       .then((response) => {
+                               if (!response.ok) {
+                                       return response.text().then((text) => {
+                                               throw new Error(`HTTP error 
${response.status}: ${text}`);
+                                       });
+                               }
+                               return response.text();
+                       })
+                       .then((previewText) => {
+                               textPreviewContent.textContent = previewText;
+                       })
+                       .catch((error) => {
+                               console.error("Error fetching email preview:", 
error);
+                               textPreviewContent.textContent = `Error loading 
preview:\n${error.message}`;
+                       });
+       }
+
+       bodyTextarea.addEventListener("input", () => {
+               clearTimeout(debounceTimeout);
+               debounceTimeout = setTimeout(fetchAndUpdateAnnouncePreview, 
debounceDelay);
+       });
+
+       fetchAndUpdateAnnouncePreview();
+
+       // Download path suffix validation
+       const pathInput = document.getElementById("download_path_suffix");
+       const pathHelpText = pathInput
+               ? pathInput.parentElement.querySelector(".form-text")
+               : null;
+
+       if (pathInput && pathHelpText) {
+               const baseText = pathHelpText.dataset.baseText || "";
+               let pathDebounce;
+
+               function updatePathHelpText() {
+                       let suffix = pathInput.value;
+                       if (suffix.includes("..") || suffix.includes("//")) {
+                               pathHelpText.textContent =
+                                       "Download path suffix must not contain 
.. or //";
+                               return;
+                       }
+                       if (suffix.startsWith("./")) {
+                               suffix = suffix.substring(1);
+                       } else if (suffix === ".") {
+                               suffix = "/";
+                       }
+                       if (!suffix.startsWith("/")) {
+                               suffix = "/" + suffix;
+                       }
+                       if (!suffix.endsWith("/")) {
+                               suffix = suffix + "/";
+                       }
+                       if (suffix.includes("/.")) {
+                               pathHelpText.textContent = "Download path 
suffix must not contain /.";
+                               return;
+                       }
+                       pathHelpText.textContent = baseText + suffix;
+               }
+
+               pathInput.addEventListener("input", () => {
+                       clearTimeout(pathDebounce);
+                       pathDebounce = setTimeout(updatePathHelpText, 10);
+               });
+               updatePathHelpText();
+       }
+});
diff --git a/atr/static/js/src/clipboard-copy.js 
b/atr/static/js/src/clipboard-copy.js
new file mode 100644
index 0000000..4e5c038
--- /dev/null
+++ b/atr/static/js/src/clipboard-copy.js
@@ -0,0 +1,34 @@
+document.addEventListener("DOMContentLoaded", () => {
+       const copyButtons = document.querySelectorAll(".atr-copy-btn");
+
+       copyButtons.forEach((button) => {
+               button.addEventListener("click", function () {
+                       const targetId = 
this.getAttribute("data-clipboard-target");
+                       const targetElement = document.querySelector(targetId);
+
+                       if (targetElement) {
+                               const textToCopy = targetElement.textContent;
+
+                               navigator.clipboard
+                                       .writeText(textToCopy)
+                                       .then(() => {
+                                               const originalText = 
this.innerHTML;
+                                               this.innerHTML = '<i class="bi 
bi-check"></i> Copied!';
+
+                                               setTimeout(() => {
+                                                       this.innerHTML = 
originalText;
+                                               }, 2000);
+                                       })
+                                       .catch((err) => {
+                                               console.error("Failed to copy: 
", err);
+                                               this.innerHTML =
+                                                       '<i class="bi 
bi-exclamation-triangle"></i> Failed!';
+
+                                               setTimeout(() => {
+                                                       this.innerHTML = '<i 
class="bi bi-clipboard"></i> Copy';
+                                               }, 2000);
+                                       });
+                       }
+               });
+       });
+});
diff --git a/atr/static/js/src/committee-directory.js 
b/atr/static/js/src/committee-directory.js
new file mode 100644
index 0000000..a8bb9cb
--- /dev/null
+++ b/atr/static/js/src/committee-directory.js
@@ -0,0 +1,166 @@
+let allCommitteeCards = [];
+
+function filterCommitteesByText() {
+       const projectFilter = document.getElementById("project-filter").value;
+       const cards = allCommitteeCards;
+       let visibleCount = 0;
+
+       if (
+               participantButton &&
+               participantButton.dataset.showing === "participant"
+       ) {
+               participantButton.dataset.showing = "all";
+               participantButton.textContent = "Show my committees";
+               participantButton.setAttribute("aria-pressed", "false");
+       }
+
+       for (const card of cards) {
+               const nameElement = card.querySelector(".card-title");
+               const name = nameElement.textContent.trim();
+               if (!projectFilter) {
+                       card.parentElement.hidden = false;
+                       visibleCount++;
+               } else {
+                       let regex;
+                       try {
+                               regex = new RegExp(projectFilter, "i");
+                       } catch (e) {
+                               const escapedFilter = projectFilter.replace(
+                                       /[.*+?^${}()|[\]\\]/g,
+                                       "\\$&",
+                               );
+                               regex = new RegExp(escapedFilter, "i");
+                       }
+                       card.parentElement.hidden = !name.match(regex);
+                       if (!card.parentElement.hidden) {
+                               visibleCount++;
+                       }
+               }
+       }
+       document.getElementById("committee-count").textContent = visibleCount;
+}
+
+document
+       .getElementById("filter-button")
+       .addEventListener("click", filterCommitteesByText);
+document
+       .getElementById("project-filter")
+       .addEventListener("keydown", (event) => {
+               if (event.key === "Enter") {
+                       filterCommitteesByText();
+                       event.preventDefault();
+               }
+       });
+
+const participantButton = document.getElementById("filter-participant-button");
+if (participantButton) {
+       participantButton.addEventListener("click", function () {
+               const showing = this.dataset.showing;
+               const cards = allCommitteeCards;
+               let visibleCount = 0;
+
+               if (showing === "all") {
+                       cards.forEach((card) => {
+                               const isParticipant = 
card.dataset.isParticipant === "true";
+                               card.parentElement.hidden = !isParticipant;
+                               if (!card.parentElement.hidden) {
+                                       visibleCount++;
+                               }
+                       });
+                       this.textContent = "Show all committees";
+                       this.dataset.showing = "participant";
+                       this.setAttribute("aria-pressed", "true");
+               } else {
+                       cards.forEach((card) => {
+                               card.parentElement.hidden = false;
+                               visibleCount++;
+                       });
+                       this.textContent = "Show my committees";
+                       this.dataset.showing = "all";
+                       this.setAttribute("aria-pressed", "false");
+               }
+               document.getElementById("project-filter").value = "";
+               document.getElementById("committee-count").textContent = 
visibleCount;
+       });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+       // Hide images that fail to load
+       document.querySelectorAll(".page-logo").forEach((img) => {
+               img.addEventListener("error", function () {
+                       this.style.display = "none";
+               });
+       });
+
+       allCommitteeCards = Array.from(
+               document.querySelectorAll(".page-project-card"),
+       );
+       const cards = allCommitteeCards;
+       const committeeCountSpan = document.getElementById("committee-count");
+       let initialVisibleCount = 0;
+       const initialShowingMode = participantButton
+               ? participantButton.dataset.showing
+               : "all";
+
+       if (participantButton) {
+               if (initialShowingMode === "participant") {
+                       participantButton.setAttribute("aria-pressed", "true");
+               } else {
+                       participantButton.setAttribute("aria-pressed", "false");
+               }
+       }
+
+       if (initialShowingMode === "participant") {
+               cards.forEach((card) => {
+                       const isParticipant = card.dataset.isParticipant === 
"true";
+                       card.parentElement.hidden = !isParticipant;
+                       if (!card.parentElement.hidden) {
+                               initialVisibleCount++;
+                       }
+               });
+       } else {
+               cards.forEach((card) => {
+                       card.parentElement.hidden = false;
+                       initialVisibleCount++;
+               });
+       }
+       committeeCountSpan.textContent = initialVisibleCount;
+
+       // Add a click listener to project subcards to handle navigation
+       // TODO: Improve accessibility
+       document.querySelectorAll(".page-project-subcard").forEach((subcard) => 
{
+               subcard.addEventListener("click", function (event) {
+                       if (this.dataset.projectUrl) {
+                               window.location.href = this.dataset.projectUrl;
+                       }
+               });
+       });
+
+       // Add a click listener for toggling project visibility within each 
committee
+       document
+               .querySelectorAll(".page-toggle-committee-projects")
+               .forEach((button) => {
+                       button.addEventListener("click", function () {
+                               const projectListContainer = this.closest(
+                                       ".page-project-list-container",
+                               );
+                               if (projectListContainer) {
+                                       const extraProjects = 
projectListContainer.querySelectorAll(
+                                               ".page-project-extra",
+                                       );
+                                       extraProjects.forEach((proj) => {
+                                               proj.classList.toggle("d-none");
+                                       });
+
+                                       const isExpanded = 
this.getAttribute("aria-expanded") === "true";
+                                       if (isExpanded) {
+                                               this.textContent = 
this.dataset.textShow;
+                                               
this.setAttribute("aria-expanded", "false");
+                                       } else {
+                                               this.textContent = 
this.dataset.textHide;
+                                               
this.setAttribute("aria-expanded", "true");
+                                       }
+                               }
+                       });
+               });
+});
diff --git a/atr/static/js/src/copy-variable.js 
b/atr/static/js/src/copy-variable.js
new file mode 100644
index 0000000..ed2ab68
--- /dev/null
+++ b/atr/static/js/src/copy-variable.js
@@ -0,0 +1,18 @@
+document.addEventListener("DOMContentLoaded", () => {
+       document.querySelectorAll(".copy-var-btn").forEach((btn) => {
+               btn.addEventListener("click", () => {
+                       const variable = btn.dataset.variable;
+                       navigator.clipboard.writeText(variable).then(() => {
+                               const originalText = btn.textContent;
+                               btn.textContent = "Copied!";
+                               btn.classList.remove("btn-outline-secondary");
+                               btn.classList.add("btn-success");
+                               setTimeout(() => {
+                                       btn.textContent = originalText;
+                                       btn.classList.remove("btn-success");
+                                       
btn.classList.add("btn-outline-secondary");
+                               }, 1500);
+                       });
+               });
+       });
+});
diff --git a/atr/static/js/src/ignore-form-change.js 
b/atr/static/js/src/ignore-form-change.js
new file mode 100644
index 0000000..a61efe7
--- /dev/null
+++ b/atr/static/js/src/ignore-form-change.js
@@ -0,0 +1,17 @@
+document.addEventListener("DOMContentLoaded", () => {
+       document
+               .querySelectorAll("table.page-details input.form-control")
+               .forEach((input) => {
+                       var row = input.closest("tr");
+                       var updateBtn = row.querySelector("button.btn-primary");
+                       function check() {
+                               if (input.value !== input.dataset.value) {
+                                       updateBtn.classList.remove("disabled");
+                               } else {
+                                       updateBtn.classList.add("disabled");
+                               }
+                       }
+                       input.addEventListener("input", check);
+                       check();
+               });
+});
diff --git a/atr/static/js/src/keys-add-toggle.js 
b/atr/static/js/src/keys-add-toggle.js
new file mode 100644
index 0000000..a0cf505
--- /dev/null
+++ b/atr/static/js/src/keys-add-toggle.js
@@ -0,0 +1,26 @@
+document.addEventListener("DOMContentLoaded", () => {
+       const checkboxes = document.querySelectorAll(
+               "input[name='selected_committees']",
+       );
+       if (checkboxes.length === 0) return;
+
+       const firstCheckbox = checkboxes[0];
+       const container = firstCheckbox.closest(".col-sm-8");
+       if (!container) return;
+
+       const button = document.createElement("button");
+       button.id = "toggleCommitteesBtn";
+       button.type = "button";
+       button.className = "btn btn-outline-secondary btn-sm mt-2";
+       button.textContent = "Select all committees";
+
+       button.addEventListener("click", () => {
+               const allChecked = Array.from(checkboxes).every((cb) => 
cb.checked);
+               checkboxes.forEach((cb) => (cb.checked = !allChecked));
+               button.textContent = allChecked
+                       ? "Select all committees"
+                       : "Deselect all committees";
+       });
+
+       container.appendChild(button);
+});
diff --git a/atr/static/js/src/ongoing-tasks-poll.js 
b/atr/static/js/src/ongoing-tasks-poll.js
new file mode 100644
index 0000000..6cec4ab
--- /dev/null
+++ b/atr/static/js/src/ongoing-tasks-poll.js
@@ -0,0 +1,119 @@
+(() => {
+       // Handle More and Less toggle buttons for collapse sections
+       document.querySelectorAll(".page-collapse-toggle").forEach((button) => {
+               button.addEventListener("click", function () {
+                       this.textContent = this.textContent.trim() === "More" ? 
"Less" : "More";
+               });
+       });
+
+       const banner = document.getElementById("ongoing-tasks-banner");
+       if (!banner) return;
+
+       const apiUrl = banner.dataset.apiUrl;
+       if (!apiUrl) return;
+
+       const countSpan = document.getElementById("ongoing-tasks-count");
+       const textSpan = document.getElementById("ongoing-tasks-text");
+       const voteButton = document.getElementById("start-vote-button");
+       const progress = document.getElementById("poll-progress");
+       const pollInterval = 3000;
+
+       let currentCount = parseInt(countSpan?.textContent || "0", 10);
+       if (currentCount === 0) return;
+
+       function restartProgress() {
+               if (!progress) return;
+               progress.style.animation = "none";
+               progress.offsetHeight;
+               progress.style.animation = `poll-grow ${pollInterval}ms linear 
forwards`;
+       }
+
+       function updateBanner(count) {
+               if (!countSpan || !textSpan) return;
+
+               currentCount = count;
+               countSpan.textContent = count;
+
+               const taskWord = count === 1 ? "task" : "tasks";
+               const isAre = count === 1 ? "is" : "are";
+               // TODO: Migrate away from setting innerHTML
+               textSpan.innerHTML = `There ${isAre} currently <strong 
id="ongoing-tasks-count">${count}</strong> background verification ${taskWord} 
running for the latest revision. Results shown below may be incomplete or 
outdated until the tasks finish.`;
+
+               if (count === 0) {
+                       // Banner always exists, but we hide it
+                       banner.classList.add("d-none");
+                       enableVoteButton();
+               }
+       }
+
+       function enableVoteButton() {
+               if (!voteButton) return;
+               if (!voteButton.classList.contains("disabled")) return;
+
+               const voteHref =
+                       voteButton.dataset.voteHref || 
voteButton.getAttribute("href");
+               if (!voteHref || voteHref === "#") return;
+
+               voteButton.classList.remove("disabled");
+               voteButton.removeAttribute("aria-disabled");
+               voteButton.removeAttribute("tabindex");
+               voteButton.removeAttribute("role");
+               voteButton.setAttribute("href", voteHref);
+               voteButton.setAttribute("title", "Start a vote on this draft");
+       }
+
+       function pollOngoingTasks() {
+               if (currentCount === 0) return;
+
+               if (progress) {
+                       progress.style.animation = "none";
+                       progress.style.width = "100%";
+                       progress.classList.remove("bg-warning");
+                       progress.classList.add(
+                               "bg-info",
+                               "progress-bar-striped",
+                               "progress-bar-animated",
+                       );
+               }
+               fetch(apiUrl)
+                       .then((response) => {
+                               if (!response.ok) throw new Error(`HTTP 
${response.status}`);
+                               return response.json();
+                       })
+                       .then((data) => {
+                               if (progress) {
+                                       progress.classList.remove(
+                                               "bg-info",
+                                               "progress-bar-striped",
+                                               "progress-bar-animated",
+                                       );
+                                       progress.classList.add("bg-warning");
+                               }
+                               const newCount = data.ongoing || 0;
+                               if (newCount !== currentCount) {
+                                       updateBanner(newCount);
+                               }
+                               if (newCount > 0) {
+                                       restartProgress();
+                                       setTimeout(pollOngoingTasks, 
pollInterval);
+                               }
+                       })
+                       .catch((error) => {
+                               console.error("Error polling ongoing tasks:", 
error);
+                               if (progress) {
+                                       progress.classList.remove(
+                                               "bg-info",
+                                               "progress-bar-striped",
+                                               "progress-bar-animated",
+                                       );
+                                       progress.classList.add("bg-warning");
+                               }
+                               restartProgress();
+                               // Double the interval when there's an error
+                               setTimeout(pollOngoingTasks, pollInterval * 2);
+                       });
+       }
+
+       restartProgress();
+       setTimeout(pollOngoingTasks, pollInterval);
+})();
diff --git a/atr/static/js/src/projects-add-form.js 
b/atr/static/js/src/projects-add-form.js
new file mode 100644
index 0000000..b77518e
--- /dev/null
+++ b/atr/static/js/src/projects-add-form.js
@@ -0,0 +1,20 @@
+document.addEventListener("DOMContentLoaded", () => {
+       const configElement = document.getElementById("projects-add-config");
+       if (!configElement) return;
+
+       const committeeDisplayName = configElement.dataset.committeeDisplayName;
+       const committeeName = configElement.dataset.committeeName;
+       if (!committeeDisplayName || !committeeName) return;
+
+       const formTexts = document.querySelectorAll(".form-text, .text-muted");
+       formTexts.forEach((element) => {
+               element.textContent = element.textContent.replace(
+                       /Example/g,
+                       committeeDisplayName,
+               );
+               element.textContent = element.textContent.replace(
+                       /example/g,
+                       committeeName.toLowerCase(),
+               );
+       });
+});
diff --git a/atr/static/js/src/projects-directory.js 
b/atr/static/js/src/projects-directory.js
new file mode 100644
index 0000000..46135ac
--- /dev/null
+++ b/atr/static/js/src/projects-directory.js
@@ -0,0 +1,74 @@
+function filter() {
+       const projectFilter = document.getElementById("project-filter").value;
+       const cards = document.querySelectorAll(".page-project-card");
+       let visibleCount = 0;
+       for (const card of cards) {
+               const nameElement = card.querySelector(".card-title");
+               const name = nameElement.innerHTML;
+               if (!projectFilter) {
+                       card.parentElement.hidden = false;
+                       visibleCount++;
+               } else {
+                       card.parentElement.hidden = !name.match(new 
RegExp(projectFilter, "i"));
+                       if (!card.parentElement.hidden) {
+                               visibleCount++;
+                       }
+               }
+       }
+       document.getElementById("project-count").textContent = visibleCount;
+}
+
+// Add event listeners
+document.getElementById("filter-button").addEventListener("click", filter);
+document
+       .getElementById("project-filter")
+       .addEventListener("keydown", (event) => {
+               if (event.key === "Enter") {
+                       filter();
+                       event.preventDefault();
+               }
+       });
+
+// Add click handlers for project cards
+document.querySelectorAll(".page-project-card").forEach((card) => {
+       card.addEventListener("click", function (event) {
+               // Prevent card navigation if click is inside a form
+               if (event.target.closest("form")) {
+                       return;
+               }
+               window.location.href = this.getAttribute("data-project-url");
+       });
+});
+
+// Participant filter logic
+const participantButton = document.getElementById("filter-participant-button");
+participantButton.addEventListener("click", function () {
+       const showing = this.dataset.showing;
+       const cards = document.querySelectorAll(".page-project-card");
+       let visibleCount = 0;
+
+       if (showing === "all") {
+               // Switch to showing only participant projects
+               cards.forEach((card) => {
+                       const isParticipant = card.dataset.isParticipant === 
"true";
+                       card.parentElement.hidden = !isParticipant;
+                       if (!card.parentElement.hidden) {
+                               visibleCount++;
+                       }
+               });
+               this.textContent = "Show all projects";
+               this.dataset.showing = "participant";
+       } else {
+               // Switch to showing all projects
+               cards.forEach((card) => {
+                       card.parentElement.hidden = false;
+                       visibleCount++;
+               });
+               this.textContent = "Show my projects";
+               this.dataset.showing = "all";
+       }
+       // Reset text filter when toggling participant view
+       document.getElementById("project-filter").value = "";
+       // Update count
+       document.getElementById("project-count").textContent = visibleCount;
+});
diff --git a/atr/static/js/src/report-results.js 
b/atr/static/js/src/report-results.js
new file mode 100644
index 0000000..ba97dac
--- /dev/null
+++ b/atr/static/js/src/report-results.js
@@ -0,0 +1,105 @@
+function toggleAllDetails() {
+       const details = document.querySelectorAll("details");
+       // Check if any are closed
+       const anyClosed = Array.from(details).some((detail) => !detail.open);
+       // If any are closed, open all
+       // Otherwise, close all
+       details.forEach((detail) => (detail.open = anyClosed));
+}
+
+function toggleStatusVisibility(type, status) {
+       const btn = document.getElementById(`btn-toggle-${type}-${status}`);
+       const targets = document.querySelectorAll(
+               `.atr-result-${type}.atr-result-status-${status}`,
+       );
+       if (!targets.length) return;
+       const elementsCurrentlyHidden = 
targets[0].classList.contains("atr-hide");
+       targets.forEach((el) => {
+               if (elementsCurrentlyHidden) {
+                       el.classList.remove("atr-hide");
+               } else {
+                       el.classList.add("atr-hide");
+               }
+       });
+       const bsSt =
+               status === "failure" || status === "exception" ? "danger" : 
status;
+       const cntMatch = btn.textContent.match(/\((\d+)\)/);
+       if (!cntMatch) {
+               console.error("Button text regex mismatch for:", 
btn.textContent);
+               return;
+       }
+       const cnt = cntMatch[0];
+       const newButtonAction = elementsCurrentlyHidden ? "Hide" : "Show";
+       btn.querySelector("span").textContent = newButtonAction;
+       if (newButtonAction === "Hide") {
+               btn.classList.remove(`btn-outline-${bsSt}`);
+               btn.classList.add(`btn-${bsSt}`);
+       } else {
+               btn.classList.remove(`btn-${bsSt}`);
+               btn.classList.add(`btn-outline-${bsSt}`);
+       }
+       if (type === "member") {
+               updateMemberStriping();
+       } else if (type === "primary") {
+               updatePrimaryStriping();
+       }
+}
+
+function restripeVisibleRows(rowSelector, stripeClass) {
+       let visibleIdx = 0;
+       document.querySelectorAll(rowSelector).forEach((row) => {
+               row.classList.remove(stripeClass);
+               const hidden =
+                       row.classList.contains("atr-hide") ||
+                       row.classList.contains("page-member-path-hide");
+               if (!hidden) {
+                       if (visibleIdx % 2 === 0) 
row.classList.add(stripeClass);
+                       visibleIdx++;
+               }
+       });
+}
+
+function updatePrimaryStriping() {
+       restripeVisibleRows(".atr-result-primary", "page-member-visible-odd");
+}
+
+function updateMemberStriping() {
+       restripeVisibleRows(".atr-result-member", "page-member-visible-odd");
+}
+
+// Toggle status visibility buttons
+document.querySelectorAll(".page-toggle-status").forEach((btn) => {
+       btn.addEventListener("click", function () {
+               const type = this.dataset.type;
+               const status = this.dataset.status;
+               toggleStatusVisibility(type, status);
+       });
+});
+
+// Toggle all details button
+const toggleAllBtn = document.getElementById("btn-toggle-all-details");
+if (toggleAllBtn) {
+       toggleAllBtn.addEventListener("click", toggleAllDetails);
+}
+
+// Member path filter
+const mpfInput = document.getElementById("member-path-filter");
+if (mpfInput) {
+       mpfInput.addEventListener("input", function () {
+               const filterText = this.value.toLowerCase();
+               document.querySelectorAll(".atr-result-member").forEach((row) 
=> {
+                       const pathCell = row.cells[0];
+                       let hide = false;
+                       if (filterText) {
+                               if 
(!pathCell.textContent.toLowerCase().includes(filterText)) {
+                                       hide = true;
+                               }
+                       }
+                       row.classList.toggle("page-member-path-hide", hide);
+               });
+               updateMemberStriping();
+       });
+}
+
+updatePrimaryStriping();
+updateMemberStriping();
diff --git a/atr/static/js/src/vote-preview.js 
b/atr/static/js/src/vote-preview.js
new file mode 100644
index 0000000..cc0cfba
--- /dev/null
+++ b/atr/static/js/src/vote-preview.js
@@ -0,0 +1,74 @@
+document.addEventListener("DOMContentLoaded", () => {
+       let debounceTimeout;
+       const debounceDelay = 500;
+
+       const bodyTextarea = document.getElementById("body");
+       const voteDurationInput = document.getElementById("vote_duration");
+       const textPreviewContent = document.getElementById(
+               "vote-body-preview-content",
+       );
+       const voteForm = document.querySelector("form.atr-canary");
+       const configElement = document.getElementById("vote-config");
+
+       if (!bodyTextarea || !voteDurationInput || !textPreviewContent || 
!voteForm) {
+               console.error("Required elements for vote preview not found. 
Exiting.");
+               return;
+       }
+
+       const previewUrl = configElement ? configElement.dataset.previewUrl : 
null;
+       const minHours = configElement ? configElement.dataset.minHours : "72";
+       const csrfTokenInput = 
voteForm.querySelector('input[name="csrf_token"]');
+
+       if (!previewUrl || !csrfTokenInput) {
+               console.error(
+                       "Required data attributes or CSRF token not found for 
vote preview.",
+               );
+               return;
+       }
+       const csrfToken = csrfTokenInput.value;
+
+       function fetchAndUpdateVotePreview() {
+               const bodyContent = bodyTextarea.value;
+               const voteDuration = voteDurationInput.value || minHours;
+
+               fetch(previewUrl, {
+                       method: "POST",
+                       headers: {
+                               "Content-Type": 
"application/x-www-form-urlencoded",
+                               "X-CSRFToken": csrfToken,
+                       },
+                       body: new URLSearchParams({
+                               body: bodyContent,
+                               duration: voteDuration,
+                               csrf_token: csrfToken,
+                       }),
+               })
+                       .then((response) => {
+                               if (!response.ok) {
+                                       return response.text().then((text) => {
+                                               throw new Error(`HTTP error 
${response.status}: ${text}`);
+                                       });
+                               }
+                               return response.text();
+                       })
+                       .then((previewText) => {
+                               textPreviewContent.textContent = previewText;
+                       })
+                       .catch((error) => {
+                               console.error("Error fetching email preview:", 
error);
+                               textPreviewContent.textContent = `Error loading 
preview:\n${error.message}`;
+                       });
+       }
+
+       bodyTextarea.addEventListener("input", () => {
+               clearTimeout(debounceTimeout);
+               debounceTimeout = setTimeout(fetchAndUpdateVotePreview, 
debounceDelay);
+       });
+
+       voteDurationInput.addEventListener("input", () => {
+               clearTimeout(debounceTimeout);
+               debounceTimeout = setTimeout(fetchAndUpdateVotePreview, 
debounceDelay);
+       });
+
+       fetchAndUpdateVotePreview();
+});
diff --git a/atr/static/js/ts/create-a-jwt.js b/atr/static/js/ts/create-a-jwt.js
new file mode 100644
index 0000000..2dd2df5
--- /dev/null
+++ b/atr/static/js/ts/create-a-jwt.js
@@ -0,0 +1,23 @@
+document.addEventListener("DOMContentLoaded", () => {
+    const form = document.getElementById("issue-jwt-form");
+    const output = document.getElementById("jwt-output");
+    if (!form || !output) {
+        return;
+    }
+    form.addEventListener("submit", async (e) => {
+        e.preventDefault();
+        const resp = await fetch(form.action, {
+            method: "POST",
+            body: new FormData(form),
+        });
+        if (resp.ok) {
+            const token = await resp.text();
+            output.classList.remove("d-none");
+            output.textContent = token;
+        }
+        else {
+            alert("Failed to fetch JWT");
+        }
+    });
+});
+//# sourceMappingURL=create-a-jwt.js.map
\ No newline at end of file
diff --git a/atr/static/js/ts/create-a-jwt.js.map 
b/atr/static/js/ts/create-a-jwt.js.map
new file mode 100644
index 0000000..f6bd847
--- /dev/null
+++ b/atr/static/js/ts/create-a-jwt.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"create-a-jwt.js","sourceRoot":"","sources":["../../ts/create-a-jwt.ts"],"names":[],"mappings":"AAAA,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAS,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAA2B,CAAC;IACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAErD,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IAED,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAQ,EAAiB,EAAE,CAAC;QACjE,CAAC,CAAC,cAAc,EAAE,CAAC;QAEnB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,
 [...]
\ No newline at end of file
diff --git a/atr/static/js/finish-selected-move.js 
b/atr/static/js/ts/finish-selected-move.js
similarity index 74%
rename from atr/static/js/finish-selected-move.js
rename to atr/static/js/ts/finish-selected-move.js
index 9bfb1da..2a659f4 100644
--- a/atr/static/js/finish-selected-move.js
+++ b/atr/static/js/ts/finish-selected-move.js
@@ -1,13 +1,4 @@
 "use strict";
-var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, 
generator) {
-    function adopt(value) { return value instanceof P ? value : new P(function 
(resolve) { resolve(value); }); }
-    return new (P || (P = Promise))(function (resolve, reject) {
-        function fulfilled(value) { try { step(generator.next(value)); } catch 
(e) { reject(e); } }
-        function rejected(value) { try { step(generator["throw"](value)); } 
catch (e) { reject(e); } }
-        function step(result) { result.done ? resolve(result.value) : 
adopt(result.value).then(fulfilled, rejected); }
-        step((generator = generator.apply(thisArg, _arguments || [])).next());
-    });
-};
 var ItemType;
 (function (ItemType) {
     ItemType["File"] = "file";
@@ -276,19 +267,19 @@ function handleDirRadio(radio) {
     }
 }
 function setState(partial) {
-    uiState = Object.assign(Object.assign({}, uiState), partial);
+    uiState = { ...uiState, ...partial };
     renderAllLists();
 }
 function onFileFilterInput(event) {
     const target = event.target;
     if (target instanceof HTMLInputElement) {
-        setState({ filters: Object.assign(Object.assign({}, uiState.filters), 
{ file: target.value }) });
+        setState({ filters: { ...uiState.filters, file: target.value } });
     }
 }
 function onDirFilterInput(event) {
     const target = event.target;
     if (target instanceof HTMLInputElement) {
-        setState({ filters: Object.assign(Object.assign({}, uiState.filters), 
{ dir: target.value }) });
+        setState({ filters: { ...uiState.filters, dir: target.value } });
     }
 }
 function onMaxFilesChange(event) {
@@ -306,60 +297,58 @@ function isErrorResponse(data) {
         (('message' in data && typeof data.message === 'string') ||
             ('error' in data && typeof data.error === 'string'));
 }
-function moveFiles(files, dest, csrfToken, signal) {
-    return __awaiter(this, void 0, void 0, function* () {
-        const formData = new FormData();
-        formData.append("csrf_token", csrfToken);
-        formData.append("variant", "MOVE_FILE");
-        for (const file of files) {
-            formData.append("source_files", file);
+async function moveFiles(files, dest, csrfToken, signal) {
+    const formData = new FormData();
+    formData.append("csrf_token", csrfToken);
+    formData.append("variant", "MOVE_FILE");
+    for (const file of files) {
+        formData.append("source_files", file);
+    }
+    formData.append("target_directory", dest);
+    try {
+        const response = await fetch(window.location.pathname, {
+            method: "POST",
+            body: formData,
+            credentials: "same-origin",
+            headers: {
+                "Accept": "application/json",
+            },
+            signal,
+        });
+        if (response.ok) {
+            return { ok: true };
         }
-        formData.append("target_directory", dest);
-        try {
-            const response = yield fetch(window.location.pathname, {
-                method: "POST",
-                body: formData,
-                credentials: "same-origin",
-                headers: {
-                    "Accept": "application/json",
-                },
-                signal,
-            });
-            if (response.ok) {
-                return { ok: true };
+        else {
+            let errorMsg = `An error occurred while moving the file (Status: 
${response.status})`;
+            if (response.status === 403)
+                errorMsg = "Permission denied to move the file.";
+            if (response.status === 400)
+                errorMsg = "Invalid request to move the file.";
+            if (signal?.aborted) {
+                errorMsg = "Move operation aborted.";
             }
-            else {
-                let errorMsg = `An error occurred while moving the file 
(Status: ${response.status})`;
-                if (response.status === 403)
-                    errorMsg = "Permission denied to move the file.";
-                if (response.status === 400)
-                    errorMsg = "Invalid request to move the file.";
-                if (signal === null || signal === void 0 ? void 0 : 
signal.aborted) {
-                    errorMsg = "Move operation aborted.";
-                }
-                try {
-                    const errorData = yield response.json();
-                    if (isErrorResponse(errorData)) {
-                        if (errorData.message) {
-                            errorMsg = errorData.message;
-                        }
-                        else if (errorData.error) {
-                            errorMsg = errorData.error;
-                        }
+            try {
+                const errorData = await response.json();
+                if (isErrorResponse(errorData)) {
+                    if (errorData.message) {
+                        errorMsg = errorData.message;
+                    }
+                    else if (errorData.error) {
+                        errorMsg = errorData.error;
                     }
                 }
-                catch ( /* Do nothing */_a) { /* Do nothing */ }
-                return { ok: false, message: errorMsg };
             }
+            catch { /* Do nothing */ }
+            return { ok: false, message: errorMsg };
         }
-        catch (error) {
-            // console.error("Network or fetch error:", error);
-            if (error instanceof Error && error.name === 'AbortError') {
-                return { ok: false, message: "Move operation aborted." };
-            }
-            return { ok: false, message: "A network error occurred. Please 
check your connection and try again." };
+    }
+    catch (error) {
+        // console.error("Network or fetch error:", error);
+        if (error instanceof Error && error.name === 'AbortError') {
+            return { ok: false, message: "Move operation aborted." };
         }
-    });
+        return { ok: false, message: "A network error occurred. Please check 
your connection and try again." };
+    }
 }
 function splitMoveCandidates(selected, dest) {
     const toMoveMutable = [];
@@ -374,42 +363,39 @@ function splitMoveCandidates(selected, dest) {
     }
     return { toMove: toMoveMutable, alreadyThere: alreadyThereMutable };
 }
-function onConfirmMoveClick() {
-    return __awaiter(this, void 0, void 0, function* () {
-        errorAlert.classList.add("d-none");
-        errorAlert.textContent = "";
-        const controller = new AbortController();
-        window.addEventListener("beforeunload", () => controller.abort());
-        if (uiState.currentlySelectedPaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
-            const { toMove, alreadyThere: itemsAlreadyInDest } = 
splitMoveCandidates(uiState.currentlySelectedPaths, 
uiState.currentlyChosenDirectoryPath);
-            if (toMove.length === 0 && itemsAlreadyInDest.length > 0 && 
uiState.currentlySelectedPaths.size > 0) {
-                errorAlert.classList.remove("d-none");
-                errorAlert.textContent = `All selected items 
(${itemsAlreadyInDest.join(", ")}) are already in the target directory. No 
items were moved.`;
-                confirmMoveButton.disabled = false;
-                return;
-            }
-            if (itemsAlreadyInDest.length > 0) {
-                const alreadyInDestMsg = `Note: ${itemsAlreadyInDest.join(", 
")} ${itemsAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
-                const existingError = errorAlert.textContent;
-                errorAlert.textContent = existingError ? `${existingError} 
${alreadyInDestMsg}` : alreadyInDestMsg;
-            }
-            const result = yield moveFiles(toMove, 
uiState.currentlyChosenDirectoryPath, uiState.csrfToken, controller.signal);
-            if (result.ok) {
-                window.location.reload();
-            }
-            else {
-                errorAlert.classList.remove("d-none");
-                errorAlert.textContent = result.message;
-            }
+async function onConfirmMoveClick() {
+    errorAlert.classList.add("d-none");
+    errorAlert.textContent = "";
+    const controller = new AbortController();
+    window.addEventListener("beforeunload", () => controller.abort());
+    if (uiState.currentlySelectedPaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
+        const { toMove, alreadyThere: itemsAlreadyInDest } = 
splitMoveCandidates(uiState.currentlySelectedPaths, 
uiState.currentlyChosenDirectoryPath);
+        if (toMove.length === 0 && itemsAlreadyInDest.length > 0 && 
uiState.currentlySelectedPaths.size > 0) {
+            errorAlert.classList.remove("d-none");
+            errorAlert.textContent = `All selected items 
(${itemsAlreadyInDest.join(", ")}) are already in the target directory. No 
items were moved.`;
+            confirmMoveButton.disabled = false;
+            return;
+        }
+        if (itemsAlreadyInDest.length > 0) {
+            const alreadyInDestMsg = `Note: ${itemsAlreadyInDest.join(", ")} 
${itemsAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
+            const existingError = errorAlert.textContent;
+            errorAlert.textContent = existingError ? `${existingError} 
${alreadyInDestMsg}` : alreadyInDestMsg;
+        }
+        const result = await moveFiles(toMove, 
uiState.currentlyChosenDirectoryPath, uiState.csrfToken, controller.signal);
+        if (result.ok) {
+            window.location.reload();
         }
         else {
             errorAlert.classList.remove("d-none");
-            errorAlert.textContent = "Please select item(s) and a destination 
directory.";
+            errorAlert.textContent = result.message;
         }
-    });
+    }
+    else {
+        errorAlert.classList.remove("d-none");
+        errorAlert.textContent = "Please select item(s) and a destination 
directory.";
+    }
 }
 document.addEventListener("DOMContentLoaded", () => {
-    var _a, _b, _c, _d;
     fileFilterInput = $(ID.fileFilter);
     fileListTableBody = $(ID.fileListTableBody);
     maxFilesInput = $(ID.maxFilesInput);
@@ -424,18 +410,18 @@ document.addEventListener("DOMContentLoaded", () => {
     let initialFilePaths = [];
     let initialTargetDirs = [];
     try {
-        const fileData = (_a = document.getElementById(ID.fileData)) === null 
|| _a === void 0 ? void 0 : _a.textContent;
+        const fileData = document.getElementById(ID.fileData)?.textContent;
         if (fileData)
             initialFilePaths = JSON.parse(fileData);
-        const dirData = (_b = document.getElementById(ID.dirData)) === null || 
_b === void 0 ? void 0 : _b.textContent;
+        const dirData = document.getElementById(ID.dirData)?.textContent;
         if (dirData)
             initialTargetDirs = JSON.parse(dirData);
     }
-    catch (_e) {
+    catch {
         // console.error("Error parsing JSON data:");
     }
-    const csrfToken = (_d = (_c = document
-        .querySelector(`#${ID.mainScriptData}`)) === null || _c === void 0 ? 
void 0 : _c.dataset.csrfToken) !== null && _d !== void 0 ? _d : null;
+    const csrfToken = document
+        .querySelector(`#${ID.mainScriptData}`)?.dataset.csrfToken ?? null;
     uiState = {
         filters: {
             file: fileFilterInput.value || "",
@@ -482,4 +468,4 @@ document.addEventListener("DOMContentLoaded", () => {
     });
     renderAllLists();
 });
-//# sourceMappingURL=finish-selected-move.js.map
+//# sourceMappingURL=finish-selected-move.js.map
\ No newline at end of file
diff --git a/atr/static/js/ts/finish-selected-move.js.map 
b/atr/static/js/ts/finish-selected-move.js.map
new file mode 100644
index 0000000..eddbb19
--- /dev/null
+++ b/atr/static/js/ts/finish-selected-move.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AAAC,CAChB,EAHK,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,aAAa;
 [...]
\ No newline at end of file
diff --git a/atr/static/js/vote-preview.js b/atr/static/js/vote-preview.js
deleted file mode 100644
index 270fe71..0000000
--- a/atr/static/js/vote-preview.js
+++ /dev/null
@@ -1,70 +0,0 @@
-document.addEventListener("DOMContentLoaded", () => {
-    let debounceTimeout;
-    const debounceDelay = 500;
-
-    const bodyTextarea = document.getElementById("body");
-    const voteDurationInput = document.getElementById("vote_duration");
-    const textPreviewContent = 
document.getElementById("vote-body-preview-content");
-    const voteForm = document.querySelector("form.atr-canary");
-    const configElement = document.getElementById("vote-config");
-
-    if (!bodyTextarea || !voteDurationInput || !textPreviewContent || 
!voteForm) {
-        console.error("Required elements for vote preview not found. 
Exiting.");
-        return;
-    }
-
-    const previewUrl = configElement ? configElement.dataset.previewUrl : null;
-    const minHours = configElement ? configElement.dataset.minHours : "72";
-    const csrfTokenInput = voteForm.querySelector('input[name="csrf_token"]');
-
-    if (!previewUrl || !csrfTokenInput) {
-        console.error("Required data attributes or CSRF token not found for 
vote preview.");
-        return;
-    }
-    const csrfToken = csrfTokenInput.value;
-
-    function fetchAndUpdateVotePreview() {
-        const bodyContent = bodyTextarea.value;
-        const voteDuration = voteDurationInput.value || minHours;
-
-        fetch(previewUrl, {
-                method: "POST",
-                headers: {
-                    "Content-Type": "application/x-www-form-urlencoded",
-                    "X-CSRFToken": csrfToken
-                },
-                body: new URLSearchParams({
-                    "body": bodyContent,
-                    "duration": voteDuration,
-                    "csrf_token": csrfToken
-                })
-            })
-            .then(response => {
-                if (!response.ok) {
-                    return response.text().then(text => {
-                        throw new Error(`HTTP error ${response.status}: 
${text}`)
-                    });
-                }
-                return response.text();
-            })
-            .then(previewText => {
-                textPreviewContent.textContent = previewText;
-            })
-            .catch(error => {
-                console.error("Error fetching email preview:", error);
-                textPreviewContent.textContent = `Error loading 
preview:\n${error.message}`;
-            });
-    }
-
-    bodyTextarea.addEventListener("input", () => {
-        clearTimeout(debounceTimeout);
-        debounceTimeout = setTimeout(fetchAndUpdateVotePreview, debounceDelay);
-    });
-
-    voteDurationInput.addEventListener("input", () => {
-        clearTimeout(debounceTimeout);
-        debounceTimeout = setTimeout(fetchAndUpdateVotePreview, debounceDelay);
-    });
-
-    fetchAndUpdateVotePreview();
-});
diff --git a/atr/template.py b/atr/template.py
index 979c995..df194bf 100644
--- a/atr/template.py
+++ b/atr/template.py
@@ -34,14 +34,16 @@ async def blank(
     content: str | htm.Element,
     description: str | None = None,
     javascripts: list[str] | None = None,
+    typescripts: list[str] | None = None,
 ) -> str:
-    js_urls = [util.static_url(f"js/{name}.js") for name in javascripts or []]
+    js_urls = [util.static_url(f"js/src/{name}.js") for name in javascripts or 
[]]
+    ts_urls = [util.static_url(f"js/ts/{name}.js") for name in typescripts or 
[]]
     return await render_sync(
         "blank.html",
         title=title,
         description=description or title,
         content=content,
-        javascripts=js_urls,
+        javascripts=js_urls + ts_urls,
     )
 
 
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index aefc386..e74102e 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -195,5 +195,5 @@
 
 {% block javascripts %}
   {{ super() }}
-  <script src="{{ static_url('js/ongoing-tasks-poll.js') }}"></script>
+  <script src="{{ static_url('js/src/ongoing-tasks-poll.js') }}"></script>
 {% endblock javascripts %}
diff --git a/atr/templates/committee-directory.html 
b/atr/templates/committee-directory.html
index eb9442c..f0c7dfb 100644
--- a/atr/templates/committee-directory.html
+++ b/atr/templates/committee-directory.html
@@ -176,5 +176,5 @@
 
 {% block javascripts %}
     {{ super() }}
-    <script src="{{ static_url('js/committee-directory.js') }}"></script>
+    <script src="{{ static_url('js/src/committee-directory.js') }}"></script>
 {% endblock javascripts %}
diff --git a/atr/templates/layouts/base.html b/atr/templates/layouts/base.html
index 01fe976..135ccba 100644
--- a/atr/templates/layouts/base.html
+++ b/atr/templates/layouts/base.html
@@ -64,7 +64,7 @@
     </main>
 
     {% block javascripts %}
-      <script src="{{ static_url('js/bootstrap.bundle.min.js') }}"></script>
+      <script src="{{ static_url('js/min/bootstrap.bundle.min.js') 
}}"></script>
     {% endblock javascripts %}
   </body>
 </html>
diff --git a/atr/templates/projects.html b/atr/templates/projects.html
index 9878905..782a633 100644
--- a/atr/templates/projects.html
+++ b/atr/templates/projects.html
@@ -84,5 +84,5 @@
 
 {% block javascripts %}
   {{ super() }}
-  <script src="{{ static_url('js/projects-directory.js') }}"></script>
+  <script src="{{ static_url('js/src/projects-directory.js') }}"></script>
 {% endblock javascripts %}
diff --git a/atr/templates/report-selected-path.html 
b/atr/templates/report-selected-path.html
index 0503f4d..2845c85 100644
--- a/atr/templates/report-selected-path.html
+++ b/atr/templates/report-selected-path.html
@@ -339,7 +339,7 @@
 
 {% block javascripts %}
   {{ super() }}
-  <script src="{{ static_url('js/report-results.js') }}"></script>
+  <script src="{{ static_url('js/src/report-results.js') }}"></script>
 {% endblock javascripts %}
 
 {% macro function_name_from_key(key) -%}
diff --git a/bootstrap/make.sh b/bootstrap/make.sh
index 597aa42..7420e48 100755
--- a/bootstrap/make.sh
+++ b/bootstrap/make.sh
@@ -22,5 +22,5 @@ cp ../reboot-shim.scss scss/reboot-shim.scss
 sass -q scss/custom.scss css/custom.css
 sed 's/custom.css.map/bootstrap.custom.css.map/g' css/custom.css > 
../../atr/static/css/bootstrap.custom.css
 sed 's/custom.css/bootstrap.custom.css/g' css/custom.css.map > 
../../atr/static/css/bootstrap.custom.css.map
-cp node_modules/bootstrap/dist/js/bootstrap.bundle.min.js 
../../atr/static/js/bootstrap.bundle.min.js
-cp node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map 
../../atr/static/js/bootstrap.bundle.min.js.map
+cp node_modules/bootstrap/dist/js/bootstrap.bundle.min.js 
../../atr/static/js/min/bootstrap.bundle.min.js
+cp node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map 
../../atr/static/js/min/bootstrap.bundle.min.js.map
diff --git a/tsconfig.json b/tsconfig.json
index a1c45e2..68eb9ca 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
 {
   "compilerOptions": {
-    "target": "ES6",
+    "target": "es2021",
     "module": "commonjs",
     "rootDir": "./atr/static/ts",
-    "outDir": "./atr/static/js",
+    "outDir": "./atr/static/js/ts",
     "strict": true,
     "esModuleInterop": true,
     "skipLibCheck": true,
@@ -11,5 +11,5 @@
     "sourceMap": true
   },
   "include": ["./atr/static/ts/**/*.ts"],
-  "exclude": ["./atr/static/js", "node_modules"]
+  "exclude": ["./atr/static/js/ts", "node_modules"]
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to