From d84dcaac6bb2b077eee9396f8db9deb9a35406b6 Mon Sep 17 00:00:00 2001 From: Liu Date: Wed, 5 Aug 2026 18:12:46 -0400 Subject: [PATCH 1/5] Publish PACIO badge verification registry --- .github/workflows/validate-badges.yml | 37 +++ README.md | 5 +- advance-directives.html | 1 + announcements.html | 1 + assets/css/badge-verification.css | 349 ++++++++++++++++++++++ assets/data/badges-public.json | 108 +++++++ assets/js/badge-verification.js | 406 ++++++++++++++++++++++++++ cognitive-status.html | 1 + docs/badge-system/README.md | 64 ++++ functional_status.html | 1 + index.html | 1 + news.html | 1 + pfe.html | 1 + privacy.html | 1 + quality_measures.html | 1 + re-assessment-timepoints.html | 1 + schema/badges-public.schema.json | 81 +++++ scripts/validate_badges.py | 169 +++++++++++ smp.html | 1 + splasch.html | 1 + terms-of-use.html | 1 + transitions-of-care.html | 1 + verification/index.html | 227 ++++++++++++++ 23 files changed, 1459 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/validate-badges.yml create mode 100644 assets/css/badge-verification.css create mode 100644 assets/data/badges-public.json create mode 100644 assets/js/badge-verification.js create mode 100644 docs/badge-system/README.md create mode 100644 schema/badges-public.schema.json create mode 100644 scripts/validate_badges.py create mode 100644 verification/index.html diff --git a/.github/workflows/validate-badges.yml b/.github/workflows/validate-badges.yml new file mode 100644 index 0000000..6372414 --- /dev/null +++ b/.github/workflows/validate-badges.yml @@ -0,0 +1,37 @@ +name: Validate public badge registry + +on: + pull_request: + paths: + - "assets/data/badges-public.json" + - "assets/badges/**" + - "schema/badges-public.schema.json" + - "scripts/validate_badges.py" + - ".github/workflows/validate-badges.yml" + push: + branches: + - gh-pages + - paciobadge + paths: + - "assets/data/badges-public.json" + - "assets/badges/**" + - "schema/badges-public.schema.json" + - "scripts/validate_badges.py" + - ".github/workflows/validate-badges.yml" + +jobs: + validate-badges: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Validate public badge registry + run: python scripts/validate_badges.py diff --git a/README.md b/README.md index 8795f9b..6bc5730 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# pacio-website-v2.0 \ No newline at end of file +# PACIO Project website + +Badge-verification publishing and privacy guidance is in +[docs/badge-system/README.md](docs/badge-system/README.md). diff --git a/advance-directives.html b/advance-directives.html index c501f51..72feff3 100644 --- a/advance-directives.html +++ b/advance-directives.html @@ -40,6 +40,7 @@ + diff --git a/announcements.html b/announcements.html index b67abc7..4a31ef9 100644 --- a/announcements.html +++ b/announcements.html @@ -40,6 +40,7 @@ + diff --git a/assets/css/badge-verification.css b/assets/css/badge-verification.css new file mode 100644 index 0000000..fc2cddd --- /dev/null +++ b/assets/css/badge-verification.css @@ -0,0 +1,349 @@ +.badge-verification-page { + padding-top: 10rem; +} + +.badge-verification-page code { + color: #1f4d78; + white-space: normal; +} + +.badge-lookup-form, +.badge-record, +.badge-registry-table-card, +.badge-disclaimer { + background: #ffffff; + border: 1px solid #d8dde6; + border-radius: 0.5rem; + box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.05); + margin-top: 1.5rem; + padding: 1.5rem; +} + +.badge-lookup-form label { + color: #12365a; + display: block; + font-family: Montserrat, sans-serif; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.03em; + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.badge-lookup-heading { + border-bottom: 1px solid #e4e8ef; + margin-bottom: 1rem; + padding-bottom: 0.75rem; +} + +.badge-lookup-heading h3 { + color: #12365a; + font-family: Montserrat, sans-serif; + font-weight: 700; + margin-bottom: 0; +} + +.badge-input-row { + display: flex; + gap: 0.75rem; +} + +.badge-id-search { + appearance: none; + background: #ffffff; + border: 2px solid #8fa2bc; + border-radius: 0.25rem; + color: #102a43; + flex: 1; + font-family: Montserrat, sans-serif; + font-size: 1.05rem; + font-weight: 700; + letter-spacing: 0.02em; + min-width: 0; + padding: 0.85rem 1rem; +} + +.badge-id-search::placeholder { + color: #7b8794; + font-weight: 400; + letter-spacing: 0; +} + +.badge-id-search:focus { + border-color: #0052b8; + box-shadow: 0 0 0 0.2rem rgba(0, 82, 184, 0.18); + outline: 0; +} + +.badge-filter-grid { + align-items: end; + display: grid; + gap: 0.9rem; + grid-template-columns: minmax(14rem, 1.5fr) minmax(8rem, 0.7fr) minmax(10rem, 1fr) minmax(12rem, 1fr) auto; + margin-top: 1.25rem; +} + +.badge-filter-control { + appearance: none; + background: #ffffff; + border: 1px solid #b9c4d4; + border-radius: 0.25rem; + color: #102a43; + font-family: Montserrat, sans-serif; + font-size: 0.95rem; + min-height: 2.75rem; + padding: 0.65rem 0.75rem; + width: 100%; +} + +select.badge-filter-control { + background-image: linear-gradient(45deg, transparent 50%, #405268 50%), linear-gradient(135deg, #405268 50%, transparent 50%); + background-position: calc(100% - 16px) 1.15rem, calc(100% - 11px) 1.15rem; + background-repeat: no-repeat; + background-size: 5px 5px, 5px 5px; + padding-right: 2rem; +} + +.badge-filter-control:focus { + border-color: #0052b8; + box-shadow: 0 0 0 0.15rem rgba(0, 82, 184, 0.16); + outline: 0; +} + +.badge-filter-actions { + white-space: nowrap; +} + +.badge-id-search:disabled, +.badge-id-search[readonly] { + background: #eef2f7; + color: #5f6b7a; + cursor: not-allowed; +} + +.badge-form-help, +.badge-record-note { + color: #5f6b7a; + font-size: 0.95rem; + margin-bottom: 0; + margin-top: 0.75rem; +} + +.badge-status-message { + color: #5f6b7a; + font-weight: 700; + margin-top: 1rem; +} + +.badge-status-message-success { + color: #1f7a3a; +} + +.badge-status-message-warning { + color: #946200; +} + +.badge-status-message-error { + color: #9b1c1c; +} + +.badge-record-header { + align-items: flex-start; + border-bottom: 1px solid #e4e8ef; + display: flex; + gap: 1rem; + justify-content: space-between; + margin-bottom: 1.25rem; + padding-bottom: 1rem; +} + +.badge-record-eyebrow { + color: #5f6b7a; + font-family: Montserrat, sans-serif; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.04em; + margin-bottom: 0.25rem; + text-transform: uppercase; +} + +.badge-record h3, +.badge-registry-table-card h3, +.badge-disclaimer h3 { + color: #12365a; + font-family: Montserrat, sans-serif; + font-weight: 700; +} + +.badge-status-pill { + border-radius: 999px; + font-family: Montserrat, sans-serif; + font-size: 0.8rem; + font-weight: 700; + padding: 0.35rem 0.75rem; + text-transform: uppercase; + white-space: nowrap; +} + +.badge-status-active { + background: #e7f5ec; + color: #1f7a3a; +} + +.badge-status-corrected, +.badge-status-retired { + background: #eef2f7; + color: #405268; +} + +.badge-status-revoked { + background: #fbe9e9; + color: #9b1c1c; +} + +.badge-status-neutral { + background: #eef2f7; + color: #405268; +} + +.badge-record-body { + display: block; +} + +.badge-record-details { + display: grid; + gap: 1rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-bottom: 0; +} + +.badge-record-details div { + border-bottom: 1px solid #e9edf3; + padding-bottom: 0.75rem; +} + +.badge-record-details dt { + color: #5f6b7a; + font-family: Montserrat, sans-serif; + font-size: 0.78rem; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.badge-record-details dd { + color: #212529; + font-size: 1rem; + margin-bottom: 0; + margin-top: 0.2rem; +} + +.badge-disclaimer { + background: #f8f9fa; +} + +.badge-registry-table-header { + align-items: flex-start; + border-bottom: 1px solid #e4e8ef; + display: flex; + gap: 1rem; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.75rem; +} + +.badge-table-count { + color: #5f6b7a; + font-family: Montserrat, sans-serif; + font-size: 0.9rem; + font-weight: 700; + margin-bottom: 0; + margin-top: 0.25rem; + white-space: nowrap; +} + +.badge-table-scroll { + border: 1px solid #d8dde6; + border-radius: 0.35rem; + margin-top: 1rem; + overflow-x: auto; +} + +.badge-registry-table { + background: #ffffff; + border-collapse: collapse; + margin-bottom: 0; + min-width: 1040px; + width: 100%; +} + +.badge-registry-table th, +.badge-registry-table td { + border-bottom: 1px solid #e4e8ef; + color: #212529; + font-size: 0.95rem; + padding: 0.8rem 0.9rem; + vertical-align: top; +} + +.badge-registry-table th { + background: #f2f4f7; + color: #12365a; + font-family: Montserrat, sans-serif; + font-size: 0.78rem; + letter-spacing: 0.03em; + text-transform: uppercase; + white-space: nowrap; +} + +.badge-registry-table tbody tr:last-child td { + border-bottom: 0; +} + +.badge-registry-table a { + color: #0052b8; + font-weight: 700; +} + +.badge-row-highlight td { + background: #fff6dc; + box-shadow: inset 0 1px 0 #f2af0d, inset 0 -1px 0 #f2af0d; +} + +.badge-row-highlight td:first-child { + box-shadow: inset 3px 0 0 #f2af0d, inset 0 1px 0 #f2af0d, inset 0 -1px 0 #f2af0d; +} + +.badge-table-empty { + color: #946200; + font-weight: 700; + margin-bottom: 0; + margin-top: 1rem; +} + +@media (max-width: 768px) { + .badge-verification-page { + padding-top: 8rem; + } + + .badge-input-row, + .badge-filter-grid, + .badge-record-header, + .badge-registry-table-header { + display: block; + } + + .badge-input-row button, + .badge-filter-actions button { + margin-top: 0.75rem; + width: 100%; + } + + .badge-record-details { + grid-template-columns: 1fr; + } + + .badge-status-pill { + display: inline-block; + margin-top: 0.75rem; + } +} diff --git a/assets/data/badges-public.json b/assets/data/badges-public.json new file mode 100644 index 0000000..cf45cc4 --- /dev/null +++ b/assets/data/badges-public.json @@ -0,0 +1,108 @@ +[ + { + "badgeId": "PACIO-BDG-2025-0001", + "eventId": "2025-01-hl7-connectathon", + "status": "Active", + "recipientDisplayName": "Patient Centric Solutions", + "recipientType": "Organization", + "companyName": "Patient Centric Solutions", + "badgeCategory": "FHIR Implementer", + "eventName": "HL7 Connectathon", + "eventDateRange": "January 2025", + "keyContributors": "Nancy Lush", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2025-0001", + "lastUpdated": "2026-07-26" + }, + { + "badgeId": "PACIO-BDG-2025-0002", + "eventId": "2025-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "Patient Centric Solutions", + "recipientType": "Organization", + "companyName": "Patient Centric Solutions", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "2025-07-01", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2025-0002", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0001", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "Matrix Care", + "recipientType": "Organization", + "companyName": "Matrix Care", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0001", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0002", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "NCQA", + "recipientType": "Organization", + "companyName": "NCQA", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0002", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0003", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "Global Alliant", + "recipientType": "Organization", + "companyName": "Global Alliant", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0003", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0004", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "Orbital Health Systems", + "recipientType": "Organization", + "companyName": "Orbital Health Systems", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0004", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0005", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "Washington DOH", + "recipientType": "Organization", + "companyName": "Washington State DOH", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0005", + "lastUpdated": "2026-08-05" + }, + { + "badgeId": "PACIO-BDG-2026-0006", + "eventId": "2026-07-cms-connectathon", + "status": "Active", + "recipientDisplayName": "MyDirectives", + "recipientType": "Organization", + "companyName": "MyDirectives, Inc.", + "badgeCategory": "FHIR Implementer", + "eventName": "CMS Connectathon", + "eventDateRange": "July 2026", + "keyContributors": "Maria Moen and Lisa Nelson", + "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0006", + "lastUpdated": "2026-08-05" + } +] diff --git a/assets/js/badge-verification.js b/assets/js/badge-verification.js new file mode 100644 index 0000000..9a93c54 --- /dev/null +++ b/assets/js/badge-verification.js @@ -0,0 +1,406 @@ +(function () { + "use strict"; + + const registryPath = "../assets/data/badges-public.json"; + const statusClasses = { + active: "badge-status-active", + corrected: "badge-status-corrected", + revoked: "badge-status-revoked", + retired: "badge-status-retired" + }; + + const form = document.getElementById("badge-verification-form"); + const input = document.getElementById("badge-id-input"); + const message = document.getElementById("badge-status-message"); + const record = document.getElementById("badge-record"); + const tableBody = document.getElementById("badge-registry-table-body"); + const tableCount = document.getElementById("badge-table-count"); + const tableEmpty = document.getElementById("badge-table-empty"); + const showAllButton = document.getElementById("badge-show-all"); + const clearFiltersButton = document.getElementById("badge-clear-filters"); + const eventFilter = document.getElementById("badge-event-filter"); + const yearFilter = document.getElementById("badge-year-filter"); + const categoryFilter = document.getElementById("badge-category-filter"); + const companyFilter = document.getElementById("badge-company-filter"); + + let badgeRecords = []; + let activeHighlightId = ""; + + function normalizeBadgeId(value) { + return (value || "").trim().toUpperCase(); + } + + function extractBadgeId(value) { + const match = (value || "").toUpperCase().match(/PACIO-BDG-\d{4}-\d{4}/); + return match ? match[0] : ""; + } + + function setMessage(text, kind) { + message.textContent = text || ""; + message.className = "badge-status-message"; + if (kind) { + message.classList.add("badge-status-message-" + kind); + } + } + + function setText(id, value) { + const element = document.getElementById(id); + if (element) { + element.textContent = value || "Not provided"; + } + } + + function setBadgeLink(id, badgeId) { + const element = document.getElementById(id); + if (!element) { + return; + } + element.textContent = ""; + if (!badgeId) { + element.textContent = "Not provided"; + return; + } + const link = document.createElement("a"); + link.href = makeLocalVerificationHref(badgeId); + link.textContent = badgeId; + link.addEventListener("click", function (event) { + event.preventDefault(); + input.value = badgeId; + verifyBadgeId(badgeId, true); + }); + element.appendChild(link); + } + + function displayValue(value) { + return (value || "").toString().trim() || "Not provided"; + } + + function companyName(item) { + return displayValue(item.companyName || item.recipientDisplayName); + } + + function eventLabel(item) { + const eventName = displayValue(item.eventName); + const eventDate = displayValue(item.eventDateRange || item.eventMonthYear); + return eventDate === "Not provided" ? eventName : eventName + " - " + eventDate; + } + + function recordYear(item) { + const eventId = item.eventId || ""; + const eventIdMatch = eventId.match(/^(20\d{2})-/); + if (eventIdMatch) { + return eventIdMatch[1]; + } + const text = [item.eventDateRange, item.eventMonthYear, item.lastUpdated, item.badgeId].join(" "); + const match = text.match(/\b(20\d{2})\b/); + return match ? match[1] : ""; + } + + function selectedFilterValue(element) { + return element ? normalizeBadgeId(element.value) : ""; + } + + function selectedTextFilterValue(element) { + return element ? (element.value || "").trim().toLowerCase() : ""; + } + + function resetRecord() { + record.hidden = true; + } + + function makeLocalVerificationHref(badgeId) { + return "../verification/?id=" + encodeURIComponent(badgeId); + } + + function statusClass(status) { + return statusClasses[(status || "").toLowerCase()] || "badge-status-neutral"; + } + + function displayRecord(item) { + const status = item.status || "Active"; + const statusPill = document.getElementById("badge-record-status"); + + document.getElementById("badge-record-title").textContent = item.recipientDisplayName || item.badgeId; + statusPill.textContent = status; + statusPill.className = "badge-status-pill " + statusClass(status); + + setText("record-badge-id", item.badgeId); + setText("record-event-id", item.eventId); + setText("record-recipient", item.recipientDisplayName); + setText("record-company", item.companyName || item.recipientDisplayName); + setText("record-recipient-type", item.recipientType); + setText("record-category", item.badgeCategory); + setText("record-event", item.eventName); + setText("record-event-date", item.eventDateRange || item.eventMonthYear); + setText("record-key-contributors", item.keyContributors); + setBadgeLink("record-supersedes", item.supersedesBadgeId); + setBadgeLink("record-superseded-by", item.supersededByBadgeId); + setText("record-updated", item.lastUpdated); + + const note = document.getElementById("record-note"); + note.textContent = item.publicNote || ""; + note.hidden = !item.publicNote; + + record.hidden = false; + } + + function addTableCell(row, text) { + const cell = document.createElement("td"); + cell.textContent = displayValue(text); + row.appendChild(cell); + return cell; + } + + function addBadgeIdCell(row, item) { + const cell = document.createElement("td"); + const link = document.createElement("a"); + link.href = makeLocalVerificationHref(item.badgeId); + link.textContent = displayValue(item.badgeId); + link.addEventListener("click", function (event) { + event.preventDefault(); + input.value = item.badgeId; + verifyBadgeId(item.badgeId, true); + }); + cell.appendChild(link); + row.appendChild(cell); + } + + function renderRegistryTable(filterValue, highlightValue) { + const filter = extractBadgeId(filterValue) || normalizeBadgeId(filterValue); + const highlight = normalizeBadgeId(highlightValue); + const selectedEvent = selectedFilterValue(eventFilter); + const selectedYear = selectedFilterValue(yearFilter); + const selectedCategory = selectedFilterValue(categoryFilter); + const selectedCompany = selectedTextFilterValue(companyFilter); + const rows = badgeRecords.filter(function (item) { + const badgeId = normalizeBadgeId(item.badgeId); + const itemEvent = normalizeBadgeId(item.eventId); + const itemCategory = normalizeBadgeId(item.badgeCategory); + const itemYear = normalizeBadgeId(recordYear(item)); + const itemCompany = companyName(item).toLowerCase(); + return (!filter || badgeId.indexOf(filter) !== -1) && + (!selectedEvent || itemEvent === selectedEvent) && + (!selectedYear || itemYear === selectedYear) && + (!selectedCategory || itemCategory === selectedCategory) && + (!selectedCompany || itemCompany.indexOf(selectedCompany) !== -1); + }); + + tableBody.textContent = ""; + rows.forEach(function (item) { + const row = document.createElement("tr"); + if (highlight && normalizeBadgeId(item.badgeId) === highlight) { + row.className = "badge-row-highlight"; + row.setAttribute("aria-current", "true"); + } + addBadgeIdCell(row, item); + addTableCell(row, item.eventName); + addTableCell(row, item.eventDateRange || item.eventMonthYear); + addTableCell(row, item.badgeCategory); + addTableCell(row, companyName(item)); + addTableCell(row, item.keyContributors); + addTableCell(row, item.status); + tableBody.appendChild(row); + }); + + tableEmpty.hidden = rows.length !== 0; + tableCount.textContent = rows.length + " public badge record" + (rows.length === 1 ? "" : "s"); + } + + function optionElement(value, label) { + const option = document.createElement("option"); + option.value = value; + option.textContent = label; + return option; + } + + function replaceOptions(select, firstLabel, options) { + select.textContent = ""; + select.appendChild(optionElement("", firstLabel)); + options.forEach(function (item) { + select.appendChild(optionElement(item.value, item.label)); + }); + } + + function uniqueSorted(values) { + return Array.from(new Set(values.filter(Boolean))).sort(); + } + + function populateFilters() { + const eventsById = {}; + badgeRecords.forEach(function (item) { + if (item.eventId && !eventsById[item.eventId]) { + eventsById[item.eventId] = eventLabel(item); + } + }); + const eventOptions = Object.keys(eventsById).sort().map(function (eventId) { + return { value: eventId, label: eventsById[eventId] }; + }); + const yearOptions = uniqueSorted(badgeRecords.map(recordYear)).map(function (year) { + return { value: year, label: year }; + }); + const categoryOptions = uniqueSorted(badgeRecords.map(function (item) { + return item.badgeCategory || ""; + })).map(function (category) { + return { value: category, label: category }; + }); + + replaceOptions(eventFilter, "All events", eventOptions); + replaceOptions(yearFilter, "All years", yearOptions); + replaceOptions(categoryFilter, "All categories", categoryOptions); + } + + function setSelectValue(select, value) { + const normalizedValue = normalizeBadgeId(value); + const option = Array.prototype.find.call(select.options, function (item) { + return normalizeBadgeId(item.value) === normalizedValue; + }); + select.value = option ? option.value : ""; + } + + function syncFiltersToRecord(item) { + setSelectValue(eventFilter, item.eventId); + setSelectValue(yearFilter, recordYear(item)); + setSelectValue(categoryFilter, item.badgeCategory); + companyFilter.value = ""; + } + + function clearFilters() { + eventFilter.value = ""; + yearFilter.value = ""; + categoryFilter.value = ""; + companyFilter.value = ""; + } + + function verifyBadgeId(rawBadgeId, updateUrl) { + const badgeId = extractBadgeId(rawBadgeId) || normalizeBadgeId(rawBadgeId); + resetRecord(); + + if (!badgeId) { + activeHighlightId = ""; + input.value = ""; + setMessage("Showing all public PACIO badge records.", "neutral"); + renderRegistryTable("", ""); + if (updateUrl) { + const url = new URL(window.location.href); + url.searchParams.delete("id"); + window.history.replaceState({}, "", url.toString()); + } + return; + } + + const match = badgeRecords.find(function (item) { + return normalizeBadgeId(item.badgeId) === badgeId; + }); + + if (!match) { + activeHighlightId = ""; + setMessage("No public PACIO badge record was found for " + badgeId + ".", "warning"); + renderRegistryTable(badgeId, ""); + if (updateUrl) { + const url = new URL(window.location.href); + url.searchParams.set("id", badgeId); + window.history.replaceState({}, "", url.toString()); + } + return; + } + + activeHighlightId = match.badgeId; + input.value = match.badgeId; + syncFiltersToRecord(match); + displayRecord(match); + renderRegistryTable(match.badgeId, match.badgeId); + setMessage("Badge record found.", "success"); + + if (updateUrl) { + const url = new URL(window.location.href); + url.searchParams.set("id", match.badgeId); + window.history.replaceState({}, "", url.toString()); + } + } + + function loadInitialBadgeId() { + const params = new URLSearchParams(window.location.search); + const badgeId = params.get("id"); + if (badgeId) { + input.value = badgeId; + verifyBadgeId(badgeId, false); + } else { + activeHighlightId = ""; + renderRegistryTable("", ""); + } + } + + function loadRegistry() { + setMessage("Loading badge registry...", "neutral"); + return fetch(registryPath, { cache: "no-store" }) + .then(function (response) { + if (!response.ok) { + throw new Error("Registry request failed with status " + response.status); + } + return response.json(); + }) + .then(function (data) { + badgeRecords = Array.isArray(data) ? data : []; + populateFilters(); + setMessage("Enter a badge ID to verify.", "neutral"); + input.disabled = false; + input.readOnly = false; + loadInitialBadgeId(); + }) + .catch(function () { + setMessage("The badge registry could not be loaded. Please try again later.", "error"); + }); + } + + form.addEventListener("submit", function (event) { + event.preventDefault(); + verifyBadgeId(input.value, true); + }); + + input.addEventListener("input", function () { + activeHighlightId = ""; + resetRecord(); + renderRegistryTable(input.value, ""); + setMessage(input.value ? "Filtering badge records as you type." : "Showing all public PACIO badge records.", "neutral"); + }); + + showAllButton.addEventListener("click", function () { + activeHighlightId = ""; + input.value = ""; + clearFilters(); + resetRecord(); + renderRegistryTable("", ""); + setMessage("Showing all public PACIO badge records.", "neutral"); + const url = new URL(window.location.href); + url.searchParams.delete("id"); + window.history.replaceState({}, "", url.toString()); + input.focus(); + }); + + [eventFilter, yearFilter, categoryFilter].forEach(function (control) { + control.addEventListener("change", function () { + activeHighlightId = ""; + resetRecord(); + renderRegistryTable(input.value, ""); + setMessage("Filtering badge records.", "neutral"); + }); + }); + + companyFilter.addEventListener("input", function () { + activeHighlightId = ""; + resetRecord(); + renderRegistryTable(input.value, ""); + setMessage("Filtering badge records.", "neutral"); + }); + + clearFiltersButton.addEventListener("click", function () { + activeHighlightId = ""; + clearFilters(); + resetRecord(); + renderRegistryTable(input.value, ""); + setMessage(input.value ? "Showing records that match the Badge ID field." : "Showing all public PACIO badge records.", "neutral"); + input.focus(); + }); + + loadRegistry(); +})(); diff --git a/cognitive-status.html b/cognitive-status.html index c0d1f77..ddfd20b 100644 --- a/cognitive-status.html +++ b/cognitive-status.html @@ -40,6 +40,7 @@ + diff --git a/docs/badge-system/README.md b/docs/badge-system/README.md new file mode 100644 index 0000000..9a3838f --- /dev/null +++ b/docs/badge-system/README.md @@ -0,0 +1,64 @@ +# PACIO Badge Verification + +This directory documents the public badge verification pieces used by the +PACIO Project website. + +## Public Components + +- `/verification/index.html` provides the public badge lookup page. +- `/assets/data/badges-public.json` contains sanitized public badge records. +- `/assets/js/badge-verification.js` performs client-side lookup by badge ID + and filtering by event, year, badge category, and company name. +- `/assets/css/badge-verification.css` styles the verification page. +- `/schema/badges-public.schema.json` documents the public registry shape. +- `/.github/workflows/validate-badges.yml` validates public badge data changes. + +## Private Components + +Do not publish the private badge administration tracker, requester contact +information, eligibility evidence, internal reviewer notes, delivery logs, or +badge-generation credentials in this repository. + +Badge generation should run from a private PACIO-controlled environment. Only +sanitized public verification records should be committed here. Generated badge +SVG files should remain in the private generation output and be delivered to +recipients as attachments. + +Public registry updates should be staged only after the private generation +package has been reviewed. The private tooling uses a separate +`publish-reviewed` step to copy the reviewed `badges-public.json` into this +repository. + +## Public Record Rules + +Public badge records may confirm the badge ID, recipient display name, public +company name, key contributors, badge category, event ID, event name, event +date, status, replacement badge links, and last updated date. + +Public badge records must not include specific implementation guides tested, +detailed test results, certification claims, conformance claims, endorsement +language, production-readiness claims, private contact information, or internal +review notes. + +## Event Organization + +Badge IDs are globally unique and stable in the format +`PACIO-BDG-YYYY-####`. Event grouping is handled separately with `eventId` in +the format `YYYY-MM-short-event-name`, for example +`2026-07-cms-connectathon`. + +The verification page supports direct badge links such as +`/verification/?id=PACIO-BDG-2026-0002` and browsing filters for event, year, +category, and company name. + +## Correction And Replacement Display + +Participant request details stay private. The public registry may show only the +resulting public status: + +- `Corrected` means the badge record was updated while keeping the same Badge + ID. +- `Retired` or `Revoked` means the badge is no longer the active recognition + record. +- `supersedesBadgeId` and `supersededByBadgeId` may be used to connect a public + replacement badge with the older badge it replaced. diff --git a/functional_status.html b/functional_status.html index ab98f52..06d55dd 100644 --- a/functional_status.html +++ b/functional_status.html @@ -40,6 +40,7 @@ + diff --git a/index.html b/index.html index 06c278d..118a566 100755 --- a/index.html +++ b/index.html @@ -41,6 +41,7 @@ + diff --git a/news.html b/news.html index 6d93368..7d935c0 100644 --- a/news.html +++ b/news.html @@ -40,6 +40,7 @@ + diff --git a/pfe.html b/pfe.html index 20046da..adca0c8 100644 --- a/pfe.html +++ b/pfe.html @@ -40,6 +40,7 @@ + diff --git a/privacy.html b/privacy.html index fe27c31..f2ad5e0 100644 --- a/privacy.html +++ b/privacy.html @@ -33,6 +33,7 @@ + diff --git a/quality_measures.html b/quality_measures.html index 3825f84..3690341 100644 --- a/quality_measures.html +++ b/quality_measures.html @@ -40,6 +40,7 @@ + diff --git a/re-assessment-timepoints.html b/re-assessment-timepoints.html index d4c6ef9..9b23ab9 100644 --- a/re-assessment-timepoints.html +++ b/re-assessment-timepoints.html @@ -40,6 +40,7 @@ + diff --git a/schema/badges-public.schema.json b/schema/badges-public.schema.json new file mode 100644 index 0000000..c44a8ac --- /dev/null +++ b/schema/badges-public.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pacioproject.org/schema/badges-public.schema.json", + "title": "PACIO Public Badge Registry", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "badgeId", + "eventId", + "status", + "recipientDisplayName", + "recipientType", + "badgeCategory", + "eventName", + "eventDateRange", + "verificationUrl", + "lastUpdated" + ], + "properties": { + "badgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "eventId": { + "type": "string", + "pattern": "^20[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*$" + }, + "status": { + "type": "string", + "enum": ["Active", "Corrected", "Revoked", "Retired"] + }, + "recipientDisplayName": { + "type": "string", + "minLength": 1 + }, + "recipientType": { + "type": "string", + "enum": ["Individual", "Team", "Organization"] + }, + "companyName": { + "type": "string" + }, + "badgeCategory": { + "type": "string", + "enum": ["Observer", "Contributor", "FHIR Implementer"] + }, + "eventName": { + "type": "string", + "minLength": 1 + }, + "eventDateRange": { + "type": "string", + "minLength": 1 + }, + "keyContributors": { + "type": "string" + }, + "supersedesBadgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "supersededByBadgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "verificationUrl": { + "type": "string", + "pattern": "^https://pacioproject\\.org/verification/\\?id=PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "lastUpdated": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + }, + "publicNote": { + "type": "string" + } + } + } +} diff --git a/scripts/validate_badges.py b/scripts/validate_badges.py new file mode 100644 index 0000000..e09bca8 --- /dev/null +++ b/scripts/validate_badges.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Validate the public PACIO badge registry before publication. + +This script intentionally uses only the Python standard library so it can run in +GitHub Actions without dependency installation. It validates the sanitized +public registry, not the private badge administration tracker. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from urllib.parse import parse_qs, urlparse + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = ROOT / "assets" / "data" / "badges-public.json" + +REQUIRED_FIELDS = { + "badgeId", + "eventId", + "status", + "recipientDisplayName", + "recipientType", + "badgeCategory", + "eventName", + "eventDateRange", + "verificationUrl", + "lastUpdated", +} +OPTIONAL_FIELDS = {"companyName", "keyContributors", "supersedesBadgeId", "supersededByBadgeId", "publicNote"} +ALLOWED_FIELDS = REQUIRED_FIELDS | OPTIONAL_FIELDS +ALLOWED_STATUSES = {"Active", "Corrected", "Revoked", "Retired"} +ALLOWED_RECIPIENT_TYPES = {"Individual", "Team", "Organization"} +ALLOWED_CATEGORIES = {"Observer", "Contributor", "FHIR Implementer"} +BADGE_ID_PATTERN = re.compile(r"^PACIO-BDG-\d{4}-\d{4}$") +EVENT_ID_PATTERN = re.compile(r"^20\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$") +DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") +EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}") +PROHIBITED_PUBLIC_FIELDS = { + "email", + "phone", + "contact", + "requesterEmail", + "requesterPhone", + "reviewerNotes", + "internalNotes", + "eligibilityEvidence", + "testResults", + "specificIgsTested", +} +PROHIBITED_TEXT = [ + "certified", + "certification", + "conformance", + "conformant", + "endorsed", + "endorsement", + "production ready", + "production-ready", + "passed testing", +] + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def load_registry() -> list[dict]: + try: + data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + except FileNotFoundError: + fail(f"Registry file not found: {REGISTRY_PATH}") + except json.JSONDecodeError as exc: + fail(f"Registry JSON is invalid: {exc}") + + if not isinstance(data, list): + fail("Registry must be a JSON array.") + return data + + +def validate_url(record: dict, index: int) -> None: + badge_id = record["badgeId"] + url = record["verificationUrl"] + parsed = urlparse(url) + if parsed.scheme != "https" or parsed.netloc != "pacioproject.org" or parsed.path != "/verification/": + fail(f"Record {index}: verificationUrl must use https://pacioproject.org/verification/.") + query_id = parse_qs(parsed.query).get("id", [None])[0] + if query_id != badge_id: + fail(f"Record {index}: verificationUrl id must match badgeId.") + + +def validate_public_text(record: dict, index: int) -> None: + combined = " ".join(str(value).lower() for value in record.values()) + for phrase in PROHIBITED_TEXT: + if phrase in combined: + fail(f"Record {index}: prohibited claim language found: {phrase}") + + +def validate_record(record: object, index: int, seen_ids: set[str]) -> None: + if not isinstance(record, dict): + fail(f"Record {index}: each registry item must be an object.") + + unknown = set(record) - ALLOWED_FIELDS + if unknown: + fail(f"Record {index}: unknown public field(s): {', '.join(sorted(unknown))}") + + prohibited = set(record) & PROHIBITED_PUBLIC_FIELDS + if prohibited: + fail(f"Record {index}: prohibited private field(s): {', '.join(sorted(prohibited))}") + + missing = REQUIRED_FIELDS - set(record) + if missing: + fail(f"Record {index}: missing required field(s): {', '.join(sorted(missing))}") + + badge_id = record["badgeId"] + if not isinstance(badge_id, str) or not BADGE_ID_PATTERN.match(badge_id): + fail(f"Record {index}: badgeId must match PACIO-BDG-YYYY-####.") + if badge_id in seen_ids: + fail(f"Record {index}: duplicate badgeId: {badge_id}") + seen_ids.add(badge_id) + + event_id = record["eventId"] + if not isinstance(event_id, str) or not EVENT_ID_PATTERN.match(event_id): + fail(f"Record {index}: eventId must match YYYY-MM-short-event-name.") + + if record["status"] not in ALLOWED_STATUSES: + fail(f"Record {index}: invalid status: {record['status']}") + if record["recipientType"] not in ALLOWED_RECIPIENT_TYPES: + fail(f"Record {index}: invalid recipientType: {record['recipientType']}") + if record["badgeCategory"] not in ALLOWED_CATEGORIES: + fail(f"Record {index}: invalid badgeCategory: {record['badgeCategory']}") + for field in ("supersedesBadgeId", "supersededByBadgeId"): + related_badge_id = record.get(field, "") + if related_badge_id: + if not isinstance(related_badge_id, str) or not BADGE_ID_PATTERN.match(related_badge_id): + fail(f"Record {index}: {field} must match PACIO-BDG-YYYY-####.") + if related_badge_id == badge_id: + fail(f"Record {index}: {field} must not match badgeId.") + if not DATE_PATTERN.match(record["lastUpdated"]): + fail(f"Record {index}: lastUpdated must use YYYY-MM-DD.") + + for field in REQUIRED_FIELDS: + if not isinstance(record[field], str) or not record[field].strip(): + fail(f"Record {index}: {field} must be a non-empty string.") + for field in OPTIONAL_FIELDS: + if field in record and not isinstance(record[field], str): + fail(f"Record {index}: {field} must be a string when provided.") + for field in ("companyName", "keyContributors"): + if EMAIL_PATTERN.search(str(record.get(field, ""))): + fail(f"Record {index}: {field} must not include email addresses.") + + validate_url(record, index) + validate_public_text(record, index) + + +def main() -> None: + registry = load_registry() + seen_ids: set[str] = set() + for index, record in enumerate(registry, start=1): + validate_record(record, index, seen_ids) + print(f"Validated {len(registry)} public badge record(s).") + + +if __name__ == "__main__": + main() diff --git a/smp.html b/smp.html index 72766bc..1962173 100644 --- a/smp.html +++ b/smp.html @@ -40,6 +40,7 @@ + diff --git a/splasch.html b/splasch.html index 7801e4d..64d24b1 100644 --- a/splasch.html +++ b/splasch.html @@ -40,6 +40,7 @@ + diff --git a/terms-of-use.html b/terms-of-use.html index 28f7f62..b6e1dec 100644 --- a/terms-of-use.html +++ b/terms-of-use.html @@ -40,6 +40,7 @@ + diff --git a/transitions-of-care.html b/transitions-of-care.html index ca5fa91..fc1ece1 100644 --- a/transitions-of-care.html +++ b/transitions-of-care.html @@ -40,6 +40,7 @@ + diff --git a/verification/index.html b/verification/index.html new file mode 100644 index 0000000..55f0813 --- /dev/null +++ b/verification/index.html @@ -0,0 +1,227 @@ + + + + + + + + + + + PACIO Badge Verification + + + + + + + + + + + + + + + +
+
+
+
+

PACIO Badge Verification

+
+
+
+

+ PACIO badges recognize participation and engagement in PACIO testing events. + A badge confirms that the listed recipient was issued the badge category for the listed event. + It does not represent certification, conformance, endorsement, production readiness, or successful completion of a specific test suite. +

+

+ Enter a badge ID below, or open a direct verification link such as + https://pacioproject.org/verification/?id=PACIO-BDG-2026-0001. +

+
+
+ +
+
+
+
+
+

Find a Badge

+

Verify by Badge ID

+
+
+ +
+ + + +
+

Type a badge ID to filter the table. Click Verify to open the matching badge record and highlight it.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + + +
+
+
+

Public Registry

+

Badge Records

+
+

+
+

+ Use the Badge ID field and filters above to browse the registry. Direct verification links filter this table automatically. +

+
+ + + + + + + + + + + + + +
Badge IDEventEvent DateCategoryCompany NameKey ContributorsStatus
+
+ +
+ +
+

Badge Meaning

+

+ PACIO badges are event-based recognition artifacts. They do not include specific implementation guides tested, + detailed testing results, certification claims, conformance claims, endorsement language, or production-readiness claims. +

+
+
+
+
+
+
+ + From 8341094a72cf74adc9f09155c75e65ff4a178a6c Mon Sep 17 00:00:00 2001 From: Liu Date: Mon, 10 Aug 2026 17:05:07 -0400 Subject: [PATCH 2/5] Update Connectathon announcement and published IG status --- announcements.html | 15 +++++++++++++++ index.html | 2 +- verification/index.html | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/announcements.html b/announcements.html index 4a31ef9..465c12f 100644 --- a/announcements.html +++ b/announcements.html @@ -58,6 +58,21 @@

PACIO Announcements

2026

    +
  • + July 14–16, 2026 +

    + The PACIO Project Community participated in a July 14–16, 2026 CMS HL7 FHIR Connectathon track, advancing interoperability testing across the PACIO Transitions of Care (ToC), Advance Healthcare Directive Interoperability (ADI), and Personal Functioning and Engagement (PFE) Implementation Guides. +

    +

    Track highlights included:

    +
      +
    • Four systems generated PACIO Transitions of Care DocumentReference and Bundle resources for delivery to FHIR servers, demonstrating the ability to produce standardized PACIO transition documents across multiple implementations.
    • +
    • PACIO Advance Healthcare Directive Interoperability (ADI) data was evaluated for emerging quality measurement use cases, establishing a foundation for future implementation and measure development.
    • +
    • QuestionnaireResponse data was transformed into PACIO Personal Functioning and Engagement (PFE) Observations, demonstrating how standardized FHIR data can support the CMS ACCESS Model by representing mobility, daily activities, participation, and environmental supports.
    • +
    +

    + Videos, presentation slides, and additional implementation details are available on the PACIO Connectathon Tracks page. +

    +
  • January 13-15, 2026

    diff --git a/index.html b/index.html index 118a566..c633a5d 100755 --- a/index.html +++ b/index.html @@ -157,7 +157,7 @@

    Use Cases

    - + Transitions of Care diff --git a/verification/index.html b/verification/index.html index 55f0813..73a6b07 100644 --- a/verification/index.html +++ b/verification/index.html @@ -24,7 +24,7 @@ - + From 992f7c31adacf950f9b8ee7bf6ccaa9be3a4022c Mon Sep 17 00:00:00 2001 From: Liu Date: Mon, 10 Aug 2026 17:14:07 -0400 Subject: [PATCH 3/5] Feature July Connectathon update on homepage --- index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index c633a5d..79e2961 100755 --- a/index.html +++ b/index.html @@ -157,7 +157,7 @@

    Use Cases

    - + Transitions of Care @@ -195,6 +195,11 @@

    Published by HL7!

    +
    +

    + July 14–16, 2026
    The PACIO Project Community participated in a CMS HL7 FHIR Connectathon track, advancing interoperability testing across the PACIO Transitions of Care (ToC), Advance Healthcare Directive Interoperability (ADI), and Personal Functioning and Engagement (PFE) Implementation Guides. Videos, presentation slides, and additional implementation details are available on the PACIO Connectathon Tracks page. +

    +

    January 13-15, 2026
    The PACIO Project Community participated in the January 2026 HL7® FHIR® Connectathon, demonstrating testing across multiple PACIO implementation guides and workflows. Testing included PROMIS-10 data capture and sharing using the PACIO Personal Functioning and Engagement (PFE) Implementation Guide, medication reconciliation and Medication Action Plan creation using the Standardized Medication Profile (SMP) Implementation Guide, and REMS workflow integration using the PACIO Transitions of Care (ToC) Implementation Guide and SMP. Additional demonstrations included AI-assisted ToC data generation, PointClickCare FHIR sandbox testing, and PACIO-focused Inferno Test Kits for automated conformance testing. From 7d629e0105b493024003f9595188f9339aa477cd Mon Sep 17 00:00:00 2001 From: Liu Date: Thu, 13 Aug 2026 13:11:21 -0400 Subject: [PATCH 4/5] Address badge verification review feedback --- assets/css/badge-verification.css | 15 +++++++++++++++ assets/data/badges-public.json | 8 ++++---- assets/js/badge-verification.js | 26 +++++++++++++------------- scripts/validate_badges.py | 16 ++++++++++++---- verification/index.html | 8 ++++---- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/assets/css/badge-verification.css b/assets/css/badge-verification.css index fc2cddd..b460fb0 100644 --- a/assets/css/badge-verification.css +++ b/assets/css/badge-verification.css @@ -111,9 +111,14 @@ select.badge-filter-control { } .badge-filter-actions { + min-width: 0; white-space: nowrap; } +.badge-filter-actions .btn { + max-width: 100%; +} + .badge-id-search:disabled, .badge-id-search[readonly] { background: #eef2f7; @@ -320,6 +325,16 @@ select.badge-filter-control { margin-top: 1rem; } +@media (max-width: 1199px) { + .badge-filter-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .badge-filter-actions { + grid-column: 1 / -1; + } +} + @media (max-width: 768px) { .badge-verification-page { padding-top: 8rem; diff --git a/assets/data/badges-public.json b/assets/data/badges-public.json index cf45cc4..e917662 100644 --- a/assets/data/badges-public.json +++ b/assets/data/badges-public.json @@ -22,7 +22,7 @@ "companyName": "Patient Centric Solutions", "badgeCategory": "FHIR Implementer", "eventName": "CMS Connectathon", - "eventDateRange": "2025-07-01", + "eventDateRange": "July 2025", "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2025-0002", "lastUpdated": "2026-08-05" }, @@ -30,14 +30,14 @@ "badgeId": "PACIO-BDG-2026-0001", "eventId": "2026-07-cms-connectathon", "status": "Active", - "recipientDisplayName": "Matrix Care", + "recipientDisplayName": "MatrixCare", "recipientType": "Organization", - "companyName": "Matrix Care", + "companyName": "MatrixCare", "badgeCategory": "FHIR Implementer", "eventName": "CMS Connectathon", "eventDateRange": "July 2026", "verificationUrl": "https://pacioproject.org/verification/?id=PACIO-BDG-2026-0001", - "lastUpdated": "2026-08-05" + "lastUpdated": "2026-08-10" }, { "badgeId": "PACIO-BDG-2026-0002", diff --git a/assets/js/badge-verification.js b/assets/js/badge-verification.js index 9a93c54..0beef7b 100644 --- a/assets/js/badge-verification.js +++ b/assets/js/badge-verification.js @@ -26,7 +26,7 @@ let badgeRecords = []; let activeHighlightId = ""; - function normalizeBadgeId(value) { + function normalizeComparisonValue(value) { return (value || "").trim().toUpperCase(); } @@ -97,7 +97,7 @@ } function selectedFilterValue(element) { - return element ? normalizeBadgeId(element.value) : ""; + return element ? normalizeComparisonValue(element.value) : ""; } function selectedTextFilterValue(element) { @@ -166,17 +166,17 @@ } function renderRegistryTable(filterValue, highlightValue) { - const filter = extractBadgeId(filterValue) || normalizeBadgeId(filterValue); - const highlight = normalizeBadgeId(highlightValue); + const filter = extractBadgeId(filterValue) || normalizeComparisonValue(filterValue); + const highlight = normalizeComparisonValue(highlightValue); const selectedEvent = selectedFilterValue(eventFilter); const selectedYear = selectedFilterValue(yearFilter); const selectedCategory = selectedFilterValue(categoryFilter); const selectedCompany = selectedTextFilterValue(companyFilter); const rows = badgeRecords.filter(function (item) { - const badgeId = normalizeBadgeId(item.badgeId); - const itemEvent = normalizeBadgeId(item.eventId); - const itemCategory = normalizeBadgeId(item.badgeCategory); - const itemYear = normalizeBadgeId(recordYear(item)); + const badgeId = normalizeComparisonValue(item.badgeId); + const itemEvent = normalizeComparisonValue(item.eventId); + const itemCategory = normalizeComparisonValue(item.badgeCategory); + const itemYear = normalizeComparisonValue(recordYear(item)); const itemCompany = companyName(item).toLowerCase(); return (!filter || badgeId.indexOf(filter) !== -1) && (!selectedEvent || itemEvent === selectedEvent) && @@ -188,7 +188,7 @@ tableBody.textContent = ""; rows.forEach(function (item) { const row = document.createElement("tr"); - if (highlight && normalizeBadgeId(item.badgeId) === highlight) { + if (highlight && normalizeComparisonValue(item.badgeId) === highlight) { row.className = "badge-row-highlight"; row.setAttribute("aria-current", "true"); } @@ -250,9 +250,9 @@ } function setSelectValue(select, value) { - const normalizedValue = normalizeBadgeId(value); + const normalizedValue = normalizeComparisonValue(value); const option = Array.prototype.find.call(select.options, function (item) { - return normalizeBadgeId(item.value) === normalizedValue; + return normalizeComparisonValue(item.value) === normalizedValue; }); select.value = option ? option.value : ""; } @@ -272,7 +272,7 @@ } function verifyBadgeId(rawBadgeId, updateUrl) { - const badgeId = extractBadgeId(rawBadgeId) || normalizeBadgeId(rawBadgeId); + const badgeId = extractBadgeId(rawBadgeId) || normalizeComparisonValue(rawBadgeId); resetRecord(); if (!badgeId) { @@ -289,7 +289,7 @@ } const match = badgeRecords.find(function (item) { - return normalizeBadgeId(item.badgeId) === badgeId; + return normalizeComparisonValue(item.badgeId) === badgeId; }); if (!match) { diff --git a/scripts/validate_badges.py b/scripts/validate_badges.py index e09bca8..a6ab3d3 100644 --- a/scripts/validate_badges.py +++ b/scripts/validate_badges.py @@ -39,6 +39,7 @@ EVENT_ID_PATTERN = re.compile(r"^20\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$") DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}") +PHONE_PATTERN = re.compile(r"(? None: fail(f"Record {index}: prohibited claim language found: {phrase}") +def validate_sensitive_public_text(record: dict, index: int) -> None: + for field, value in record.items(): + if not isinstance(value, str): + continue + if EMAIL_PATTERN.search(value): + fail(f"Record {index}: {field} must not include email addresses.") + if PHONE_PATTERN.search(value): + fail(f"Record {index}: {field} must not include phone numbers.") + + def validate_record(record: object, index: int, seen_ids: set[str]) -> None: if not isinstance(record, dict): fail(f"Record {index}: each registry item must be an object.") @@ -149,12 +160,9 @@ def validate_record(record: object, index: int, seen_ids: set[str]) -> None: for field in OPTIONAL_FIELDS: if field in record and not isinstance(record[field], str): fail(f"Record {index}: {field} must be a string when provided.") - for field in ("companyName", "keyContributors"): - if EMAIL_PATTERN.search(str(record.get(field, ""))): - fail(f"Record {index}: {field} must not include email addresses.") - validate_url(record, index) validate_public_text(record, index) + validate_sensitive_public_text(record, index) def main() -> None: diff --git a/verification/index.html b/verification/index.html index 73a6b07..118d5b8 100644 --- a/verification/index.html +++ b/verification/index.html @@ -23,8 +23,8 @@ - - + + @@ -42,7 +42,7 @@

  • - +
@@ -59,7 +59,7 @@

PACIO Badge Verification

PACIO badges recognize participation and engagement in PACIO testing events. - A badge confirms that the listed recipient was issued the badge category for the listed event. + A badge confirms that the listed recipient participated in the listed event and category. It does not represent certification, conformance, endorsement, production readiness, or successful completion of a specific test suite.

From b1019e6df800e327ec3d8b19aca69a2a969863d3 Mon Sep 17 00:00:00 2001 From: Liu Date: Thu, 13 Aug 2026 13:48:07 -0400 Subject: [PATCH 5/5] Refine badge verification and navigation layout --- advance-directives.html | 2 +- announcements.html | 2 +- assets/css/badge-verification.css | 9 +++------ assets/js/badge-verification.js | 26 +++++++++++++------------- cognitive-status.html | 2 +- css/styles.css | 5 +++-- functional_status.html | 2 +- index.html | 4 ++-- news.html | 2 +- pfe.html | 2 +- privacy.html | 2 +- quality_measures.html | 2 +- re-assessment-timepoints.html | 2 +- smp.html | 2 +- splasch.html | 2 +- terms-of-use.html | 2 +- transitions-of-care.html | 2 +- verification/index.html | 2 +- 18 files changed, 35 insertions(+), 37 deletions(-) diff --git a/advance-directives.html b/advance-directives.html index 72feff3..397bbd4 100644 --- a/advance-directives.html +++ b/advance-directives.html @@ -40,7 +40,7 @@

- + diff --git a/announcements.html b/announcements.html index 465c12f..7d60bae 100644 --- a/announcements.html +++ b/announcements.html @@ -40,7 +40,7 @@ - + diff --git a/assets/css/badge-verification.css b/assets/css/badge-verification.css index b460fb0..3de8865 100644 --- a/assets/css/badge-verification.css +++ b/assets/css/badge-verification.css @@ -79,7 +79,7 @@ align-items: end; display: grid; gap: 0.9rem; - grid-template-columns: minmax(14rem, 1.5fr) minmax(8rem, 0.7fr) minmax(10rem, 1fr) minmax(12rem, 1fr) auto; + grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 1.25rem; } @@ -111,12 +111,13 @@ select.badge-filter-control { } .badge-filter-actions { + grid-column: 1 / -1; min-width: 0; white-space: nowrap; } .badge-filter-actions .btn { - max-width: 100%; + white-space: nowrap; } .badge-id-search:disabled, @@ -329,10 +330,6 @@ select.badge-filter-control { .badge-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - - .badge-filter-actions { - grid-column: 1 / -1; - } } @media (max-width: 768px) { diff --git a/assets/js/badge-verification.js b/assets/js/badge-verification.js index 0beef7b..84775a4 100644 --- a/assets/js/badge-verification.js +++ b/assets/js/badge-verification.js @@ -26,7 +26,7 @@ let badgeRecords = []; let activeHighlightId = ""; - function normalizeComparisonValue(value) { + function normalizeFilterValue(value) { return (value || "").trim().toUpperCase(); } @@ -97,7 +97,7 @@ } function selectedFilterValue(element) { - return element ? normalizeComparisonValue(element.value) : ""; + return element ? normalizeFilterValue(element.value) : ""; } function selectedTextFilterValue(element) { @@ -166,17 +166,17 @@ } function renderRegistryTable(filterValue, highlightValue) { - const filter = extractBadgeId(filterValue) || normalizeComparisonValue(filterValue); - const highlight = normalizeComparisonValue(highlightValue); + const filter = extractBadgeId(filterValue) || normalizeFilterValue(filterValue); + const highlight = normalizeFilterValue(highlightValue); const selectedEvent = selectedFilterValue(eventFilter); const selectedYear = selectedFilterValue(yearFilter); const selectedCategory = selectedFilterValue(categoryFilter); const selectedCompany = selectedTextFilterValue(companyFilter); const rows = badgeRecords.filter(function (item) { - const badgeId = normalizeComparisonValue(item.badgeId); - const itemEvent = normalizeComparisonValue(item.eventId); - const itemCategory = normalizeComparisonValue(item.badgeCategory); - const itemYear = normalizeComparisonValue(recordYear(item)); + const badgeId = normalizeFilterValue(item.badgeId); + const itemEvent = normalizeFilterValue(item.eventId); + const itemCategory = normalizeFilterValue(item.badgeCategory); + const itemYear = normalizeFilterValue(recordYear(item)); const itemCompany = companyName(item).toLowerCase(); return (!filter || badgeId.indexOf(filter) !== -1) && (!selectedEvent || itemEvent === selectedEvent) && @@ -188,7 +188,7 @@ tableBody.textContent = ""; rows.forEach(function (item) { const row = document.createElement("tr"); - if (highlight && normalizeComparisonValue(item.badgeId) === highlight) { + if (highlight && normalizeFilterValue(item.badgeId) === highlight) { row.className = "badge-row-highlight"; row.setAttribute("aria-current", "true"); } @@ -250,9 +250,9 @@ } function setSelectValue(select, value) { - const normalizedValue = normalizeComparisonValue(value); + const normalizedValue = normalizeFilterValue(value); const option = Array.prototype.find.call(select.options, function (item) { - return normalizeComparisonValue(item.value) === normalizedValue; + return normalizeFilterValue(item.value) === normalizedValue; }); select.value = option ? option.value : ""; } @@ -272,7 +272,7 @@ } function verifyBadgeId(rawBadgeId, updateUrl) { - const badgeId = extractBadgeId(rawBadgeId) || normalizeComparisonValue(rawBadgeId); + const badgeId = extractBadgeId(rawBadgeId) || normalizeFilterValue(rawBadgeId); resetRecord(); if (!badgeId) { @@ -289,7 +289,7 @@ } const match = badgeRecords.find(function (item) { - return normalizeComparisonValue(item.badgeId) === badgeId; + return normalizeFilterValue(item.badgeId) === badgeId; }); if (!match) { diff --git a/cognitive-status.html b/cognitive-status.html index ddfd20b..448b65e 100644 --- a/cognitive-status.html +++ b/cognitive-status.html @@ -40,7 +40,7 @@ - + diff --git a/css/styles.css b/css/styles.css index c96597e..4098cac 100755 --- a/css/styles.css +++ b/css/styles.css @@ -10097,6 +10097,7 @@ img::-moz-selection { font-size: 0.95rem; color: #fff; letter-spacing: 0.0625em; + white-space: nowrap; } #mainNav .navbar-nav .nav-item .nav-link.active, #mainNav .navbar-nav .nav-item .nav-link:hover { color: #f2af0d; @@ -10155,7 +10156,7 @@ body:not(:has(header.masthead)) #mainNav:not(.navbar-shrink) { height: 2.5rem; } #mainNav .navbar-nav .nav-item { - margin-right: 1rem; + margin-right: 0.5rem; } #mainNav .navbar-nav .nav-item:last-child { margin-right: 0; @@ -10627,4 +10628,4 @@ ul.news li { div.video { margin-bottom: 2em; -} \ No newline at end of file +} diff --git a/functional_status.html b/functional_status.html index 06d55dd..eb80f60 100644 --- a/functional_status.html +++ b/functional_status.html @@ -40,7 +40,7 @@ - + diff --git a/index.html b/index.html index 79e2961..cbe63f1 100755 --- a/index.html +++ b/index.html @@ -22,7 +22,7 @@ - + @@ -41,7 +41,7 @@ - + diff --git a/news.html b/news.html index 7d935c0..8b8334c 100644 --- a/news.html +++ b/news.html @@ -40,7 +40,7 @@ - + diff --git a/pfe.html b/pfe.html index adca0c8..10fe88e 100644 --- a/pfe.html +++ b/pfe.html @@ -40,7 +40,7 @@ - + diff --git a/privacy.html b/privacy.html index f2ad5e0..f933d3e 100644 --- a/privacy.html +++ b/privacy.html @@ -33,7 +33,7 @@ - + diff --git a/quality_measures.html b/quality_measures.html index 3690341..0a1b42d 100644 --- a/quality_measures.html +++ b/quality_measures.html @@ -40,7 +40,7 @@ - + diff --git a/re-assessment-timepoints.html b/re-assessment-timepoints.html index 9b23ab9..4762263 100644 --- a/re-assessment-timepoints.html +++ b/re-assessment-timepoints.html @@ -40,7 +40,7 @@ - + diff --git a/smp.html b/smp.html index 1962173..18fd666 100644 --- a/smp.html +++ b/smp.html @@ -40,7 +40,7 @@ - + diff --git a/splasch.html b/splasch.html index 64d24b1..69a2fcb 100644 --- a/splasch.html +++ b/splasch.html @@ -40,7 +40,7 @@ - + diff --git a/terms-of-use.html b/terms-of-use.html index b6e1dec..7e018d5 100644 --- a/terms-of-use.html +++ b/terms-of-use.html @@ -40,7 +40,7 @@ - + diff --git a/transitions-of-care.html b/transitions-of-care.html index fc1ece1..0cddc9f 100644 --- a/transitions-of-care.html +++ b/transitions-of-care.html @@ -40,7 +40,7 @@ - + diff --git a/verification/index.html b/verification/index.html index 118d5b8..232db57 100644 --- a/verification/index.html +++ b/verification/index.html @@ -23,7 +23,7 @@ - +