diff --git a/.ds.baseline b/.ds.baseline index c64d22627..56c3afc7d 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 125, + "line_number": 123, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-11-14T15:53:44Z" + "generated_at": "2024-11-21T23:08:45Z" } diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f002bb3fc..c3ef5dcbb 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -144,6 +144,7 @@ jobs: inputs: requirements.txt ignore-vulns: | PYSEC-2024-60 + PYSEC-2022-43162 - name: Run npm audit run: make npm-audit diff --git a/app/__init__.py b/app/__init__.py index 64580fcc1..4d89cd59e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,8 +1,9 @@ import os import pathlib +import secrets from functools import partial from time import monotonic -from urllib.parse import urlparse, urlunparse +from urllib.parse import unquote, urlparse, urlunparse import jinja2 from flask import ( @@ -114,6 +115,7 @@ from notifications_utils.formatters import ( get_lines_with_normalised_whitespace, ) from notifications_utils.recipients import format_phone_number_human_readable +from notifications_utils.url_safe_token import generate_token login_manager = LoginManager() csrf = CSRFProtect() @@ -168,10 +170,38 @@ def create_app(application): @application.context_processor def inject_feature_flags(): - feature_best_practices_enabled = application.config[ - "FEATURE_BEST_PRACTICES_ENABLED" - ] - return dict(FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled) + feature_best_practices_enabled = application.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + feature_about_page_enabled = application.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + return dict( + FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled, + FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled, + ) + + @application.context_processor + def inject_initial_signin_url(): + ttl = 24 * 60 * 60 + + # make and store the state + state = generate_token( + str(request.remote_addr), + current_app.config["SECRET_KEY"], + current_app.config["DANGEROUS_SALT"], + ) + + state_key = f"login-state-{unquote(state)}" + redis_client.set(state_key, state, ex=ttl) + + # make and store the nonce + nonce = secrets.token_urlsafe() + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(nonce_key, nonce, ex=ttl) + + url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") + if url is not None: + url = url.replace("NONCE", nonce) + url = url.replace("STATE", state) + + return {'initial_signin_url': url} notify_environment = os.environ["NOTIFY_ENVIRONMENT"] diff --git a/app/assets/images/alarm.svg b/app/assets/images/alarm.svg new file mode 100644 index 000000000..57e6180bc --- /dev/null +++ b/app/assets/images/alarm.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/alert.svg b/app/assets/images/alert.svg new file mode 100644 index 000000000..d0d516d8f --- /dev/null +++ b/app/assets/images/alert.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/calendar.svg b/app/assets/images/calendar.svg new file mode 100644 index 000000000..9b755e9fd --- /dev/null +++ b/app/assets/images/calendar.svg @@ -0,0 +1 @@ + diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index da5d77bf2..e57146b14 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -676,28 +676,28 @@ details form { margin-top: 0; } -ol.best-practices-list { +ol.guides-list { counter-reset: item; list-style-type: none; padding-left: 0; } -ol.best-practices-list.set-two { +ol.guides-list.set-two { counter-reset: item 6; } -ol.best-practices-list.set-three { +ol.guides-list.set-three { counter-reset: item 10; } -ol.best-practices-list li { +ol.guides-list li { counter-increment: item; margin-bottom: 15px; position: relative; padding-left: 40px; } -ol.best-practices-list li::before { +ol.guides-list li::before { content: counter(item); background-color: #005ea2; color: white; @@ -713,11 +713,11 @@ ol.best-practices-list li::before { top: 0; } -li.best-practices { +li.guides { padding-bottom: 50px; } -div.best-practices { +div.guides { height: 400px } @@ -820,7 +820,7 @@ $do-dont-top-bar-width: 1; } @media (max-width: 758px) { - .best-practices-flex-container { + .guides-flex-container { flex-direction: column; } } @@ -898,7 +898,7 @@ li.linked-card:hover svg, display: block; } -.about-icon-list { +.icon-list { display: flex; width: 24px; height: 24px; @@ -908,10 +908,6 @@ li.linked-card:hover svg, margin-right: 4px; } -.usa-icon-list__content{ - padding-left: 0; -} - .indented-paragraph { margin-left: calc(24px + 4px); margin-top: 4px; diff --git a/app/config.py b/app/config.py index f40b46dea..146230047 100644 --- a/app/config.py +++ b/app/config.py @@ -91,9 +91,7 @@ class Config(object): getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" ) - FEATURE_ABOUT_PAGE_ENABLED = ( - getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" - ) + FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/index.py b/app/main/views/index.py index 5e7bff008..06f07d38c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -1,11 +1,15 @@ -import os -import secrets -from urllib.parse import unquote - -from flask import abort, current_app, redirect, render_template, request, url_for +from flask import ( + abort, + current_app, + jsonify, + redirect, + render_template, + request, + url_for, +) from flask_login import current_user -from app import redis_client, status_api_client +from app import status_api_client from app.formatters import apply_html_class, convert_markdown_template from app.main import main from app.main.views.pricing import CURRENT_SMS_RATE @@ -16,55 +20,42 @@ from app.main.views.sub_navigation_dictionaries import ( using_notify_nav, ) from app.utils.user import user_is_logged_in -from notifications_utils.url_safe_token import generate_token # Hook to check for feature flags @main.before_request def check_feature_flags(): - if ( - request.path.startswith("/guides/best-practices") - and not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + if request.path.startswith("/guides") and not current_app.config.get( + "FEATURE_BEST_PRACTICES_ENABLED", False ): abort(404) - if ( - request.path.startswith("/about") - and not current_app.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + if request.path.startswith("/about") and not current_app.config.get( + "FEATURE_ABOUT_PAGE_ENABLED", False ): abort(404) +@main.route("/test/feature-flags") +def test_feature_flags(): + return jsonify( + { + "FEATURE_BEST_PRACTICES_ENABLED": current_app.config[ + "FEATURE_BEST_PRACTICES_ENABLED" + ] + } + ) + + @main.route("/") def index(): if current_user and current_user.is_authenticated: return redirect(url_for("main.choose_account")) - ttl = 24 * 60 * 60 - - # make and store the state - state = generate_token( - str(request.remote_addr), - current_app.config["SECRET_KEY"], - current_app.config["DANGEROUS_SALT"], - ) - state_key = f"login-state-{unquote(state)}" - redis_client.set(state_key, state, ex=ttl) - - # make and store the nonce - nonce = secrets.token_urlsafe() - nonce_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(nonce_key, nonce, ex=ttl) - - url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") - if url is not None: - url = url.replace("NONCE", nonce) - url = url.replace("STATE", state) return render_template( "views/signedout.html", sms_rate=CURRENT_SMS_RATE, - counts=status_api_client.get_count_of_live_services_and_organizations(), - initial_signin_url=url, + counts=status_api_client.get_count_of_live_services_and_organizations() ) @@ -216,61 +207,61 @@ def trial_mode_new(): @user_is_logged_in def best_practices(): return render_template( - "views/best-practices/best-practices.html", + "views/guides/best-practices.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/clear-goals") +@main.route("/guides/clear-goals") @user_is_logged_in def clear_goals(): return render_template( - "views/best-practices/clear-goals.html", + "views/guides/clear-goals.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/rules-and-regulations") +@main.route("/guides/rules-and-regulations") @user_is_logged_in def rules_and_regulations(): return render_template( - "views/best-practices/rules-and-regulations.html", + "views/guides/rules-and-regulations.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/establish-trust") +@main.route("/guides/establish-trust") @user_is_logged_in def establish_trust(): return render_template( - "views/best-practices/establish-trust.html", + "views/guides/establish-trust.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/write-for-action") +@main.route("/guides/write-for-action") @user_is_logged_in def write_for_action(): return render_template( - "views/best-practices/write-for-action.html", + "views/guides/write-for-action.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/multiple-languages") +@main.route("/guides/multiple-languages") @user_is_logged_in def multiple_languages(): return render_template( - "views/best-practices/multiple-languages.html", + "views/guides/multiple-languages.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/benchmark-performance") +@main.route("/guides/benchmark-performance") @user_is_logged_in def benchmark_performance(): return render_template( - "views/best-practices/benchmark-performance.html", + "views/guides/benchmark-performance.html", navigation_links=best_practices_nav(), ) @@ -287,6 +278,13 @@ def guidance_index(): ) +@main.route("/contact") +def contact(): + return render_template( + "views/about/contact.html", +) + + @main.route("/about") def about_notify(): return render_template( @@ -295,10 +293,18 @@ def about_notify(): ) -@main.route("/about/contact") -def contact(): +@main.route("/about/security") +def about_security(): return render_template( - "views/about/contact.html", + "views/about/security.html", + navigation_links=about_notify_nav(), + ) + + +@main.route("/about/why-text-messaging") +def why_text_messaging(): + return render_template( + "views/about/why-text-messaging.html", navigation_links=about_notify_nav(), ) diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index 56505ca6e..16991297a 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -1,3 +1,6 @@ +from flask import current_app + + def features_nav(): return [ { @@ -22,46 +25,20 @@ def features_nav(): def using_notify_nav(): - return [ - { - "name": "Get started", - "link": "main.get_started", - }, - { - "name": "Guides", - "link": "main.best_practices", - }, - { - "name": "Trial mode", - "link": "main.trial_mode_new", - }, - { - "name": "Tracking usage", - "link": "main.pricing", - }, - { - "name": "Delivery status", - "link": "main.message_status", - }, - { - "name": "Guidance", - "link": "main.guidance_index", - # "sub_navigation_items": [ - # { - # "name": "Formatting", - # "link": "main.edit_and_format_messages", - # }, - # { - # "name": "Send files by email", - # "link": "main.send_files_by_email", - # }, - # ] - # { - # "name": "API documentation", - # "link": "main.documentation", - # }, - }, + nav_items = [ + {"name": "Get started", "link": "main.get_started"}, + {"name": "Guides", "link": "main.best_practices"}, + {"name": "Trial mode", "link": "main.trial_mode_new"}, + {"name": "Tracking usage", "link": "main.pricing"}, + {"name": "Delivery Status", "link": "main.message_status"}, + {"name": "Guidance", "link": "main.guidance_index"}, ] + if not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED"): + nav_items = [ + item for item in nav_items if item["link"] != "main.best_practices" + ] + + return nav_items def best_practices_nav(): @@ -110,8 +87,32 @@ def best_practices_nav(): def about_notify_nav(): return [ { - "name": "About notify", + "name": "About Notify", "link": "main.about_notify", + "sub_navigation_items": [ + { + "name": "Why text messaging", + "link": "main.why_text_messaging", + "sub_sub_navigation_items": [ + { + "name": "Reach people using a common method", + "link": "main.why_text_messaging#reach-people-using-a-common-method", + }, + { + "name": "Improve customer experience", + "link": "main.why_text_messaging#improve-customer-experience", + }, + { + "name": "What texting is best for", + "link": "main.why_text_messaging#what-texting-is-best-for", + }, + ], + }, + { + "name": "Security", + "link": "main.about_security", + }, + ], }, { "name": "Contact", diff --git a/app/navigation.py b/app/navigation.py index a02df484d..271d6848b 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -53,7 +53,7 @@ class HeaderNavigation(Navigation): "establish_trust", "write_for_action", "multiple_languages", - "benchmark_performance" + "benchmark_performance", }, "using_notify": { "get_started", diff --git a/app/templates/base.html b/app/templates/base.html index 369e62b81..b2b6639e9 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -66,7 +66,20 @@ diff --git a/app/templates/components/folder-path.html b/app/templates/components/folder-path.html index c686702ba..b6615a2da 100644 --- a/app/templates/components/folder-path.html +++ b/app/templates/components/folder-path.html @@ -22,7 +22,7 @@ {{ folder.name }} {% endif %} {% else %} - Templates + Templates {% endif %} {% if not loop.last %}{{ folder_path_separator() }}{% endif %} {% endif %} diff --git a/app/templates/components/best-practices/cards.html b/app/templates/components/guides/cards.html similarity index 100% rename from app/templates/components/best-practices/cards.html rename to app/templates/components/guides/cards.html diff --git a/app/templates/components/best-practices/circle_number.html b/app/templates/components/guides/circle_number.html similarity index 100% rename from app/templates/components/best-practices/circle_number.html rename to app/templates/components/guides/circle_number.html diff --git a/app/templates/components/header.html b/app/templates/components/header.html index 248abf250..a1c1de389 100644 --- a/app/templates/components/header.html +++ b/app/templates/components/header.html @@ -43,18 +43,29 @@ secondary_navigation.is_selected('settings')},
-
diff --git a/app/templates/components/best-practices/nav_breadcrumb.html b/app/templates/components/nav_breadcrumb.html similarity index 55% rename from app/templates/components/best-practices/nav_breadcrumb.html rename to app/templates/components/nav_breadcrumb.html index fb5124356..83305cf9b 100644 --- a/app/templates/components/best-practices/nav_breadcrumb.html +++ b/app/templates/components/nav_breadcrumb.html @@ -1,8 +1,12 @@ +{% macro breadcrumb(title, parent_menu_title, url) %} +{% endmacro %} diff --git a/app/templates/partials/jobs/notifications.html b/app/templates/partials/jobs/notifications.html index 379a8efef..6df3085ee 100644 --- a/app/templates/partials/jobs/notifications.html +++ b/app/templates/partials/jobs/notifications.html @@ -2,7 +2,7 @@ {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} -
+
{% if job.scheduled %}

diff --git a/app/templates/views/about/about.html b/app/templates/views/about/about.html index 39dfce671..17180cef7 100644 --- a/app/templates/views/about/about.html +++ b/app/templates/views/about/about.html @@ -1,13 +1,12 @@ {% extends "base.html" %} -{% set page_title = "About notify" %} +{% set page_title = "About Notify" %} {% block per_page_title %} {{page_title}} {% endblock %} {% block content_column_content %} -

{{page_title}}

Notify.gov is a text messaging service built by and for the government. We help agencies communicate more @@ -17,8 +16,9 @@

  • Meet people where they are
  • More effectively deliver program outcomes
  • Save administrative costs
  • -
  • Implement 21st Century IDEA and other directives
  • +
  • Implement 21st Century + IDEA and other directives
  • Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users can create an account and get started within minutes. We take the security and privacy of messaging data seriously @@ -56,9 +56,9 @@

      {% for item in product_highlights %}
    • -
      +
      - {{item.card_heading}} @@ -68,10 +68,8 @@
    • {% endfor %}
    -

    See if Notify is right for you

    -

    Notify.gov is a product of the Public Benefits Studio, a product accelerator inside +

    See if Notify is right for you

    +

    Notify.gov is a product of the Public Benefits Studio, a product accelerator inside the federal government.

    - -
    {% endblock %} diff --git a/app/templates/views/about/security.html b/app/templates/views/about/security.html new file mode 100644 index 000000000..0a916035e --- /dev/null +++ b/app/templates/views/about/security.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} + +{% set page_title = "Security" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{{ breadcrumbs.breadcrumb(page_title, "About", "main.about_notify") }} + +
    +

    {{page_title}}

    +

    Notify.gov is built for the needs of government agencies with fundamental system + security processes in place to: +

    +
      +
    • protect user data
    • +
    • keep systems secure
    • +
    • manage risks around information
    • +
    +

    + Notify.gov operates under a full three-year Authority-to-Operate (ATO). This + federal security authorization process leverages security + controls provided by National Institute of Standards and Technology (NIST). +

    + +

    + Our infrastructure runs on cloud.gov and utilizes several + services through Amazon Web + Services (AWS), including + AWS SNS for sending SMS + messages. +

    +

    For more information about the Notify.gov infrastructure, contact us at notify-support@gsa.gov.

    +

    Data

    +

    + On Notify.gov, data is encrypted both in transit and at rest. To send a message, agencies upload a spreadsheet of + phone numbers and other necessary data from their existing data management system. +

    +

    + Notify.gov is not a system of record, so it does not have a System of Records Notice (SORN). Agencies are + responsible for managing their data outside of Notify.gov. +

    +

    Data retention

    +

    + Any data uploads that have recipient data are held for seven calendar days; personally identifiable information + (PII) is never stored in Notify’s database. +

    +

    Multi-Factor Authentication

    +

    + Notify.gov uses Login.gov for enhanced security. + Login.gov is an extra layer of security created by the government that uses multi-factor authentication and stronger + passwords to protect your account. +

    +

    + To access Notify.gov, users will use a Login.gov account associated with their agency (.gov) email with one of the + multi-factor authentication + methods offered through Login.gov. +

    +
    +{% endblock %} diff --git a/app/templates/views/about/why-text-messaging.html b/app/templates/views/about/why-text-messaging.html new file mode 100644 index 000000000..2b1982225 --- /dev/null +++ b/app/templates/views/about/why-text-messaging.html @@ -0,0 +1,87 @@ +{% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} + +{% set page_title = "Why text messaging" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{{ breadcrumbs.breadcrumb(page_title, "About", "main.about_notify") }} + +
    +

    {{page_title}}

    +

    Reach people using a common method

    +

    + Confusing or unreceived notifications are one of the largest barriers to people getting and keeping + benefits. The typical ways the government communicates with people often fall short. Low income households are more + likely to experience housing instability, which means paper mail, already slow, can easily be missed. +

    + +

    + Pew Research shows that nearly all adults in the US have a cell phone. Reliance on smartphones + for online access is especially common among Americans with lower household incomes and those with lower levels of + formal education. Of those earning less than $30,000 a year, 28% say their mobile phone is the sole method to + digitally connect. +

    +

    + This means that for many people who rely on government services, cell phones may be the most reliable place to meet + people where they already are. +

    +

    Improve customer experience

    +

    + Text messages can deliver concise information and drive an audience to take action quickly. Timely reminders sent + via text message have been proven to decrease re-enrollment churn and save money for administering agencies. +

    +

    + Texting not only helps programs reach people using a nearly-universal communication method, it is a cost effective + way to do so. With Notify.gov you can get started for free, allowing you to try out + texting to complement your existing communications and outreach strategies. +

    +

    What texting is best for

    +

    + Agencies, like you, are already using Notify.gov to text about the following programs. +

    + {% set card_contents = [ + { + "image_src": asset_url('images/calendar.svg'), + "card_heading": "Reminders", + "p_text": "In a text bubble // Your Quality Control food phone interview is on ((date)) at ((time)). Failure to + attend may lead to closure of your benefits. Call 1-800-222-3333 with questions.", + "alt_text": "reminder text example" + }, + { + "image_src": asset_url('images/alert.svg'), + "card_heading": "Alerts to take action", + "p_text": "In a text bubble // Your household's Medicaid coverage is expiring. To keep getting Medicaid, you must + complete your renewal by ((date)). You can renew online at dhs.state.gov…", + "alt_text": "alerts text example" + }, + { + "image_src": asset_url('images/alarm.svg'), + "card_heading": "Important status updates", + "p_text": "In a text bubble // Your passport has been issued at the Los Angeles Passport Agency. Please come to the + desk between 1:30pm and 2:30pm today to pick up your passport…", + "alt_text": "status update text example" + }, + ] %} + {% for item in card_contents %} +
    +
    +
    +

    {{item.card_heading}}

    +

    {{item.p_text}}

    +
    + {% if item.image_src %} + {{ item.alt_text }} + {% endif %} +
    +
    + {% endfor %} +
    +{% endblock %} diff --git a/app/templates/views/best-practices/benchmark-performance.html b/app/templates/views/guides/benchmark-performance.html similarity index 93% rename from app/templates/views/best-practices/benchmark-performance.html rename to app/templates/views/guides/benchmark-performance.html index 2661f6b00..5e24dc9cd 100644 --- a/app/templates/views/best-practices/benchmark-performance.html +++ b/app/templates/views/guides/benchmark-performance.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} {% set page_title = "Measuring performance with benchmarking" %} @@ -7,7 +8,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    Learn how effective your texting program can be.

    @@ -19,7 +21,7 @@

    What other texting studies have found

    - When the Center on Budget and Policy Priorities studied WIC, they found key learnings about the quantity of messages delivered, how people engage with messages, and how they take action. @@ -78,7 +80,8 @@

    - The Code for America’s Texting + The Code for America’s Texting Playbook reported specific learnings around appointment reminders, completing document submission, and maintenance reminders. diff --git a/app/templates/views/best-practices/best-practices.html b/app/templates/views/guides/best-practices.html similarity index 78% rename from app/templates/views/best-practices/best-practices.html rename to app/templates/views/guides/best-practices.html index 7711dea9b..4784f72c4 100644 --- a/app/templates/views/best-practices/best-practices.html +++ b/app/templates/views/guides/best-practices.html @@ -1,11 +1,12 @@ {% extends "base.html" %} +{% set page_title = "Best Practices" %} + {% block per_page_title %} -Best Practices +{{page_title}} {% endblock %} {% block content_column_content %} -

    Best Practices

    For texting the public

    @@ -23,41 +24,41 @@ Best Practices "svg_src": "goal", "card_heading": "Establish clear goals", "p_text": "Start with a singular purpose. Make explicit what you want to achieve.", - "link": "/guides/best-practices/clear-goals" + "link": "/guides/clear-goals" }, { "svg_src": "compliant", "card_heading": "Follow rules & regulations", "p_text": "Understand what is required when texting the public.", - "link": "/guides/best-practices/rules-and-regulations" + "link": "/guides/rules-and-regulations" }, { "svg_src": "trust", "card_heading": "Establish trust", "p_text": "Help your audience anticipate and welcome your texts.", - "link": "/guides/best-practices/establish-trust" + "link": "/guides/establish-trust" }, { "svg_src": "runner", "card_heading": "Write texts that provoke action", "p_text": "Help your audience know what to do with the information you send.", - "link": "/guides/best-practices/write-for-action" + "link": "/guides/write-for-action" }, { "svg_src": "language", "card_heading": "Send texts in multiple languages", "p_text": "What to know as you plan translated texts.", - "link": "/guides/best-practices/multiple-languages" + "link": "/guides/multiple-languages" }, { "svg_src": "chart", "card_heading": "Measure performance with benchmarking", "p_text": "Learn how effective your texting program can be.", - "link": "/guides/best-practices/benchmark-performance" + "link": "/guides/benchmark-performance" } ] %} - {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + {% with card_contents=card_contents %}{% include "components/guides/cards.html" %}{% endwith %}
    diff --git a/app/templates/views/best-practices/clear-goals.html b/app/templates/views/guides/clear-goals.html similarity index 97% rename from app/templates/views/best-practices/clear-goals.html rename to app/templates/views/guides/clear-goals.html index b67684c04..71ed0e1a3 100644 --- a/app/templates/views/best-practices/clear-goals.html +++ b/app/templates/views/guides/clear-goals.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} {% set page_title = "Establish clear goals" %} @@ -7,7 +8,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    Start with a singular purpose. Make explicit what you want to achieve.

    @@ -160,7 +162,7 @@

    Review your drafted hypothesis with your team to make sure everyone is aligned on your desired goals. A clear and - concise hypothesis can help you decide how to write text message + concise hypothesis can help you decide how to write text message content that provokes action.

    diff --git a/app/templates/views/best-practices/establish-trust.html b/app/templates/views/guides/establish-trust.html similarity index 94% rename from app/templates/views/best-practices/establish-trust.html rename to app/templates/views/guides/establish-trust.html index fc26e2a71..3383315ba 100644 --- a/app/templates/views/best-practices/establish-trust.html +++ b/app/templates/views/guides/establish-trust.html @@ -1,5 +1,7 @@ {% extends "base.html" %} -{% from "components/best-practices/circle_number.html" import circle_number %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} + +{% from "components/guides/circle_number.html" import circle_number %} {% set page_title = "Establish trust" %} @@ -8,7 +10,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    Help your audience anticipate and welcome your texts.

    @@ -38,7 +41,7 @@
  • To reinforce legitimacy, include these key messages in your outreach:

    -
      +
      1. Introduce yourself – Give people time to get to know your texting communications before you need direct action from them
      2. Phone number – Note the sending phone number your texts will come from
      3. @@ -79,7 +82,7 @@
        • -
            +
            1. Identify yourself in the text message. Use a consistent and familiar program name to precede each text you send.
            2. Personalize with first name. Through A/B testing, Code for America saw an increase in @@ -98,7 +101,7 @@

              {{circle_number(7) }}Dept. of Social Services: Hi {{circle_number(8) }}Julie, Your Medicaid renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}https://www.application.yourstate.gov or {{circle_number(10) }} call the number on the back of your Medicaid card.

              @@ -114,14 +117,14 @@

              If you're sending one-way notifications, phone carriers allow a single auto-response message that will be generated if a recipient tries to text a response to your message. Use the auto-response to reaffirm your key messages around legitimacy and communicate to recipients that texts are coming from an automated system.

              -
                +
                1. Provide a way to contact a human in any auto-response text
                2. Provide information on how to opt out and opt back in
        • -
          +
          Phone

          @@ -167,7 +170,7 @@ }, ] %} - {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + {% with card_contents=card_contents %}{% include "components/guides/cards.html" %}{% endwith %}

          Prepare your team

          {% set card_contents = [ @@ -187,7 +190,7 @@ pages or within case management notes.", }, ] %} - {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + {% with card_contents=card_contents %}{% include "components/guides/cards.html" %}{% endwith %}
  • {% endblock %} diff --git a/app/templates/views/best-practices/multiple-languages.html b/app/templates/views/guides/multiple-languages.html similarity index 87% rename from app/templates/views/best-practices/multiple-languages.html rename to app/templates/views/guides/multiple-languages.html index 13c638d38..dd9c3727f 100644 --- a/app/templates/views/best-practices/multiple-languages.html +++ b/app/templates/views/guides/multiple-languages.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} {% set page_title = "Text in multiple languages" %} @@ -7,7 +8,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    What to know as you plan translated texts.

    diff --git a/app/templates/views/best-practices/rules-and-regulations.html b/app/templates/views/guides/rules-and-regulations.html similarity index 94% rename from app/templates/views/best-practices/rules-and-regulations.html rename to app/templates/views/guides/rules-and-regulations.html index c9a95a122..db06ab76e 100644 --- a/app/templates/views/best-practices/rules-and-regulations.html +++ b/app/templates/views/guides/rules-and-regulations.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} {% set page_title = "Rules and regulations" %} @@ -7,7 +8,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    Understand what is required when texting the public.

    @@ -20,7 +22,7 @@ If you do need expressed consent, consider including a pre-checked plain language opt-in (i.e. “It’s OK to text me.”) on digital forms. Be sure to ask for an up-to-date phone number and include a question about the recipient’s preferred - language for text messages if you expect to translate your text + language for text messages if you expect to translate your text messages in languages other than English.

    @@ -79,7 +81,7 @@

    Opting out

    There is no policy requirement for senders to communicate opt-out options, but including instructions in introductory and/or + href="../guides/establish-trust#as-people-receive-texts"> including instructions in introductory and/or auto-response texts on how to opt out and opt back in are effective ways to establish trust with your audience.

    diff --git a/app/templates/views/best-practices/write-for-action.html b/app/templates/views/guides/write-for-action.html similarity index 90% rename from app/templates/views/best-practices/write-for-action.html rename to app/templates/views/guides/write-for-action.html index d97b74d8b..d3019596c 100644 --- a/app/templates/views/best-practices/write-for-action.html +++ b/app/templates/views/guides/write-for-action.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% from "components/best-practices/circle_number.html" import circle_number %} +{% import "components/nav_breadcrumb.html" as breadcrumbs %} +{% from "components/guides/circle_number.html" import circle_number %} {% set page_title = "Write texts that provoke action" %} @@ -8,7 +9,8 @@ {% endblock %} {% block content_column_content %} -{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +{{ breadcrumbs.breadcrumb(page_title, "Guides", "main.best_practices") }} +

    {{page_title}}

    Help your audience know what to do with the information you send.

    @@ -40,11 +42,11 @@
  • Use a neutral, direct, and professional tone. This works better than a friendly or overly casual tone.
  • Build conditions for action

    -
      +
      • For example, getting a person to update their mailing address:

        -
          +
          1. Clearly state the information and the response you want the recipient to take.
          2. Point directly to where the action can be completed, not to more information.
          3. Make sure the action can be completed via mobile phone, like calling a person or going to a @@ -78,7 +80,8 @@

      What provoking action looks like

      -

      Evidence +

      Evidence shows that employing behavioral science is an effective way to increase the likelihood of a recipient @@ -100,7 +103,7 @@ internet services that belong to you.", }, ] %} - {% with card_contents=card_contents, text_align='left' %}{% include "components/best-practices/cards.html" %}{% + {% with card_contents=card_contents, text_align='left' %}{% include "components/guides/cards.html" %}{% endwith %}

    {% endblock %} diff --git a/app/utils/csv.py b/app/utils/csv.py index 4ed6d16b5..79e3535c8 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -103,6 +103,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] for header in original_column_headers: if header.lower() != "phone number": @@ -118,6 +119,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] yield ",".join(fieldnames) + "\n" @@ -140,6 +142,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] for header in original_column_headers: if header.lower() != "phone number": @@ -158,6 +161,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] yield Spreadsheet.from_rows([map(str, values)]).as_csv_data diff --git a/deploy-config/production.yml b/deploy-config/production.yml index 42a681cb4..9f5cffc89 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -1,6 +1,6 @@ env: production instances: 2 -memory: 1.5G +memory: 2G command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: beta.notify.gov cloud_dot_gov_route: notify.app.cloud.gov diff --git a/package-lock.json b/package-lock.json index 64db0c270..c6d584bcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/stream": "^3.0.1", - "@uswds/uswds": "^3.9.0", + "@uswds/uswds": "^3.10.0", "cbor-js": "0.1.0", "d3": "^7.9.0", "govuk_frontend_toolkit": "^9.0.1", @@ -21,10 +21,10 @@ "hogan": "1.0.2", "jquery": "3.7.1", "morphdom": "^2.7.4", - "playwright": "^1.48.2", + "playwright": "^1.49.0", "python": "^0.0.4", "query-command-supported": "1.0.0", - "sass-embedded": "^1.80.6", + "sass-embedded": "^1.82.0", "textarea-caret": "3.1.0", "timeago": "1.6.7", "vinyl-buffer": "^1.0.1", @@ -50,7 +50,7 @@ "jest-environment-jsdom": "^29.2.2", "jshint": "2.13.6", "jshint-stylish": "2.2.1", - "rollup": "^4.24.4", + "rollup": "^4.28.0", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-node-resolve": "5.2.0" }, @@ -2667,216 +2667,234 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz", - "integrity": "sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", + "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz", - "integrity": "sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", + "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz", - "integrity": "sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", + "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz", - "integrity": "sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", + "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz", - "integrity": "sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", + "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz", - "integrity": "sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", + "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz", - "integrity": "sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", + "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz", - "integrity": "sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", + "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz", - "integrity": "sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", + "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz", - "integrity": "sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", + "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz", - "integrity": "sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", + "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz", - "integrity": "sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", + "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz", - "integrity": "sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", + "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz", - "integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz", - "integrity": "sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", + "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz", - "integrity": "sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", + "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz", - "integrity": "sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", + "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz", - "integrity": "sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", + "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3485,11 +3503,10 @@ } }, "node_modules/@uswds/uswds": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.9.0.tgz", - "integrity": "sha512-8THm36j7iLjrDiI1D0C6b3hHsmM/Sy5Iiz+IjE+i/gYzVUMG9XVthxAZYonhU97Q1b079n6nYwlUmDSYowJecQ==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.10.0.tgz", + "integrity": "sha512-LWFTQzp4e3kqtnD/Wsyfx9uGTkn5GEpzhscNWJMIsdWBGKtiu96QT99oRJUmcsB6HbGhR0Th0FtlK/Zzx2WghA==", "dependencies": { - "object-assign": "4.1.1", "receptor": "1.0.0", "resolve-id-refs": "0.1.0" }, @@ -5146,10 +5163,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8314,7 +8332,8 @@ "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -12011,11 +12030,11 @@ } }, "node_modules/playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dependencies": { - "playwright-core": "1.48.2" + "playwright-core": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -12028,9 +12047,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", "bin": { "playwright-core": "cli.js" }, @@ -12951,9 +12970,10 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.4.tgz", - "integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", + "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", + "license": "MIT", "dependencies": { "@types/estree": "1.0.6" }, @@ -12965,24 +12985,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.4", - "@rollup/rollup-android-arm64": "4.24.4", - "@rollup/rollup-darwin-arm64": "4.24.4", - "@rollup/rollup-darwin-x64": "4.24.4", - "@rollup/rollup-freebsd-arm64": "4.24.4", - "@rollup/rollup-freebsd-x64": "4.24.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.4", - "@rollup/rollup-linux-arm-musleabihf": "4.24.4", - "@rollup/rollup-linux-arm64-gnu": "4.24.4", - "@rollup/rollup-linux-arm64-musl": "4.24.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.4", - "@rollup/rollup-linux-riscv64-gnu": "4.24.4", - "@rollup/rollup-linux-s390x-gnu": "4.24.4", - "@rollup/rollup-linux-x64-gnu": "4.24.4", - "@rollup/rollup-linux-x64-musl": "4.24.4", - "@rollup/rollup-win32-arm64-msvc": "4.24.4", - "@rollup/rollup-win32-ia32-msvc": "4.24.4", - "@rollup/rollup-win32-x64-msvc": "4.24.4", + "@rollup/rollup-android-arm-eabi": "4.28.0", + "@rollup/rollup-android-arm64": "4.28.0", + "@rollup/rollup-darwin-arm64": "4.28.0", + "@rollup/rollup-darwin-x64": "4.28.0", + "@rollup/rollup-freebsd-arm64": "4.28.0", + "@rollup/rollup-freebsd-x64": "4.28.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", + "@rollup/rollup-linux-arm-musleabihf": "4.28.0", + "@rollup/rollup-linux-arm64-gnu": "4.28.0", + "@rollup/rollup-linux-arm64-musl": "4.28.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", + "@rollup/rollup-linux-riscv64-gnu": "4.28.0", + "@rollup/rollup-linux-s390x-gnu": "4.28.0", + "@rollup/rollup-linux-x64-gnu": "4.28.0", + "@rollup/rollup-linux-x64-musl": "4.28.0", + "@rollup/rollup-win32-arm64-msvc": "4.28.0", + "@rollup/rollup-win32-ia32-msvc": "4.28.0", + "@rollup/rollup-win32-x64-msvc": "4.28.0", "fsevents": "~2.3.2" } }, @@ -13091,16 +13111,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass-embedded": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.6.tgz", - "integrity": "sha512-Og4aqBnaA3oJfIpHaLuNATAqzBRgUJDYJy2X15V59cot2wYOtiT/ciPnyuq1o7vpDEeOkHhEd+mSviSlXoETug==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.82.0.tgz", + "integrity": "sha512-v13sRVVZtWAQLpAGTz5D8hy+oyNKRHao5tKVc/P6AMqSP+jDM8X6GkEpL0jfbu3MaN2/hAQsd4Qx14GG1u0prQ==", + "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", - "immutable": "^4.0.0", + "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "bin": { @@ -13110,35 +13132,36 @@ "node": ">=16.0.0" }, "optionalDependencies": { - "sass-embedded-android-arm": "1.80.6", - "sass-embedded-android-arm64": "1.80.6", - "sass-embedded-android-ia32": "1.80.6", - "sass-embedded-android-riscv64": "1.80.6", - "sass-embedded-android-x64": "1.80.6", - "sass-embedded-darwin-arm64": "1.80.6", - "sass-embedded-darwin-x64": "1.80.6", - "sass-embedded-linux-arm": "1.80.6", - "sass-embedded-linux-arm64": "1.80.6", - "sass-embedded-linux-ia32": "1.80.6", - "sass-embedded-linux-musl-arm": "1.80.6", - "sass-embedded-linux-musl-arm64": "1.80.6", - "sass-embedded-linux-musl-ia32": "1.80.6", - "sass-embedded-linux-musl-riscv64": "1.80.6", - "sass-embedded-linux-musl-x64": "1.80.6", - "sass-embedded-linux-riscv64": "1.80.6", - "sass-embedded-linux-x64": "1.80.6", - "sass-embedded-win32-arm64": "1.80.6", - "sass-embedded-win32-ia32": "1.80.6", - "sass-embedded-win32-x64": "1.80.6" + "sass-embedded-android-arm": "1.82.0", + "sass-embedded-android-arm64": "1.82.0", + "sass-embedded-android-ia32": "1.82.0", + "sass-embedded-android-riscv64": "1.82.0", + "sass-embedded-android-x64": "1.82.0", + "sass-embedded-darwin-arm64": "1.82.0", + "sass-embedded-darwin-x64": "1.82.0", + "sass-embedded-linux-arm": "1.82.0", + "sass-embedded-linux-arm64": "1.82.0", + "sass-embedded-linux-ia32": "1.82.0", + "sass-embedded-linux-musl-arm": "1.82.0", + "sass-embedded-linux-musl-arm64": "1.82.0", + "sass-embedded-linux-musl-ia32": "1.82.0", + "sass-embedded-linux-musl-riscv64": "1.82.0", + "sass-embedded-linux-musl-x64": "1.82.0", + "sass-embedded-linux-riscv64": "1.82.0", + "sass-embedded-linux-x64": "1.82.0", + "sass-embedded-win32-arm64": "1.82.0", + "sass-embedded-win32-ia32": "1.82.0", + "sass-embedded-win32-x64": "1.82.0" } }, "node_modules/sass-embedded-android-arm": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.80.6.tgz", - "integrity": "sha512-UeUKMTRsnz4/dh7IzvhjONxa4/jmVp539CHDd8VZOsqg9M3HcNJNIkUzQWbuwZ+nSlWrTuo7Tvn3XlypopCBzw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.82.0.tgz", + "integrity": "sha512-ttGMvWnA/5TYdZTjr5fWHDbb9nZgKipHKCc9zZQRF5HjUydOYWKNqmAJHQtbFWaq35kd5qn6yiE73IJN6eJ6wA==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -13148,12 +13171,13 @@ } }, "node_modules/sass-embedded-android-arm64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.80.6.tgz", - "integrity": "sha512-4rC4ZGM/k4ENVjLXnK3JTst8e8FI9MHSol2Fl7dCdYyJ3KLnlt4qL4AEYfU8zq1tcBb7CBOSZVR+CzCKubnXdg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.82.0.tgz", + "integrity": "sha512-bldHMs02QQWXsgHUZRgolNnZdMjN6XHvmUYoRkzmFq7lsvtLU6SJg2S1Wa9IZJs9jRWdTmOgA6YibSf3pROyFQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -13163,12 +13187,13 @@ } }, "node_modules/sass-embedded-android-ia32": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.80.6.tgz", - "integrity": "sha512-Lxz2SXE2KdHnynuHF+D6flDvrd55/zaEAWUeka9MxEr6FmR66d8UBOIy5ETwCSUd//S/SE5Jl6oTnHppgD1zNA==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.82.0.tgz", + "integrity": "sha512-FUJOnxw8IYKuYuxxiOkk6QXle8/yQFtKjnuSAJuZ5ZpLVMcSZzLc3SWOtuEXYx5iSAfJCO075o2ZoG/pPrJ9aw==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -13178,12 +13203,13 @@ } }, "node_modules/sass-embedded-android-riscv64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.80.6.tgz", - "integrity": "sha512-hKdxY/oOqB+JJhSoBTDM5DJO1j/xtxQgayh2cLCCUx37IQQe3SEdc3V2JFf/4mIo5peaS4cjqwwSATF+l2zaXg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.82.0.tgz", + "integrity": "sha512-rd+vc+sxJxNnbhaubiIJmnb1b3FvC9wxCIq8spstopbO7o1uufvBBDeRoFSJaN+7oNhamzjlYGdu6aQoQNs3+A==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -13193,12 +13219,13 @@ } }, "node_modules/sass-embedded-android-x64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.80.6.tgz", - "integrity": "sha512-Eap2Fi3kTx/rVLBsOnOp5RYPr5+lFjTZ652zR24dmYFe9/sDgasakJIOPjOvD2bRuL9z0uWEY1AXVeeOPeZKrg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.82.0.tgz", + "integrity": "sha512-EVlybGTgJ8wNLyWj8RUatPXSnmIcvCsx3EfsRfBfhGihLbn4NNpavYO9QsvZzI2XWbJqHLBCd+CvkTcDw/TaSQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -13208,12 +13235,13 @@ } }, "node_modules/sass-embedded-darwin-arm64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.80.6.tgz", - "integrity": "sha512-0mnAx8Vq6Gxj3PQt3imgITfK33hhqrSKpyHSuab71gZZni5opsdtoggq2JawW+1taRFTEZwbZJLKZ0MBDbwCCA==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.82.0.tgz", + "integrity": "sha512-LvdJPojjKlNGYOB0nSUR/ZtMDuAF4puspHlwK42aA/qK292bfSkMUKZPPapB2aSRwccc/ieBq5fI7n/WHrOCVw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -13223,12 +13251,13 @@ } }, "node_modules/sass-embedded-darwin-x64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.80.6.tgz", - "integrity": "sha512-Ib20yNZFOrJ7YVT+ltoe+JQNKPcRclM3iLAK69XZZYcSeFM/72SCoQBAaVGIpT23dxDp7FXiE4lO602c3xTRwQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.82.0.tgz", + "integrity": "sha512-6LfnD6YmG1aBfd3ReqMOJDb6Pg2Z/hmlJB7nU+Lb3E+hCNjAZAgeUHQxU/Pm1eIqJJTU/h4ib5QP0Pt9O8yVnw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -13238,12 +13267,13 @@ } }, "node_modules/sass-embedded-linux-arm": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.80.6.tgz", - "integrity": "sha512-QR0Q6TZox/ThuU2r9c0s3fKCgU2rXAEocpitdgxFp6tta+GsQlMFV3oON2unAa8Bwnuxkmf0YOaK0Oy/TwzkXw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.82.0.tgz", + "integrity": "sha512-ozjdC5rWzyi5Vo300I4tVZzneXOTQUiaxOr7DjtN26HuFaGAGCGmvThh2BRV4RvySg++5H9rdFu+VgyUQ5iukw==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13253,12 +13283,13 @@ } }, "node_modules/sass-embedded-linux-arm64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.80.6.tgz", - "integrity": "sha512-n5r98pBXawrQQKaxIYCMM1zDpnngsqxTkOrmvsYLFiAMCSbR0lWf/7sBB33k/Pm0D6dsbp3jpHilCoQNKI3jIw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.82.0.tgz", + "integrity": "sha512-590/y0HJr/JiyxaqgR7Xf9P20BIhJ+zhB/afAnVuZe/4lEfCpTyM5xMe2+sKLsqtrVyzs9Zm/M4S4ASUOPCggA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13268,12 +13299,13 @@ } }, "node_modules/sass-embedded-linux-ia32": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.80.6.tgz", - "integrity": "sha512-O6dWZdcOkryRdDCxVMGOeVowgblpDgVcAuRtZ1F1X7XfbpDriTQm64D+9vVZIrywYSPoJfQMJJ662cr0wUs9IQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.82.0.tgz", + "integrity": "sha512-hpc4acZ3UTjjJ3Q/GUXqQOCSml6AFKaku0HMawra9bKyRmOpxn8V5hqgXeOWVjK2oQzCmCnJvwKoQUP+S/SIYQ==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13283,12 +13315,13 @@ } }, "node_modules/sass-embedded-linux-musl-arm": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.80.6.tgz", - "integrity": "sha512-X9FC8s8fvQGRiXc+eATlZ57N44Iq3nNa0M0ugi3ysdJwkaNYvOeS4QzBHKQAaw3QiTqdxTnLUHHVBkyzdCi9pw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.82.0.tgz", + "integrity": "sha512-R5PQmY/I+GSoMtfLo8GgHkvF/q6x6y8VNM7yu/Ac1mJj86n48VFi29W1HfY2496+Q6cpAq7toobDj7YfldIdVA==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13298,12 +13331,13 @@ } }, "node_modules/sass-embedded-linux-musl-arm64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.80.6.tgz", - "integrity": "sha512-VeUSHUi3MAsvOlg9QI4X/2j04h1659aE+7qKP/282CYBTrGkjFGSXZhIki9WKWDgIpDiSInRYXfQQRWhPhjCDg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.82.0.tgz", + "integrity": "sha512-bc2MUSMv/jabnNGEyKP2jQAYZoEzTT/c633W6QoeSEWETGCuTNjaHvWWE6qSI6/UfRg1EpuV1LQA2jPMzZfv/w==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13313,12 +13347,13 @@ } }, "node_modules/sass-embedded-linux-musl-ia32": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.80.6.tgz", - "integrity": "sha512-GqitS2Nab8ah0+wfCqaxW1hnI1piC08FimL6+lM9YWK5DbCOOF82IapbvJOy0feUmd/wNnHmyNTgE9h0zVMFdQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.82.0.tgz", + "integrity": "sha512-ZQKCFKm5TBcJ19UG6uUQmIKfVCJIWMb7e1a93lGeujSb9gyKF5Fb6MN3tuExoT7iFK8zU0Z9iyHqh93F58lcCw==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13328,12 +13363,13 @@ } }, "node_modules/sass-embedded-linux-musl-riscv64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.80.6.tgz", - "integrity": "sha512-ySs15z7QSRRQK/aByEEqaJLYW/sTpfynefNPZCtsVNVEzNRwy+DRpxNChtxo+QjKq97ocXETbdG5KLik7QOTJg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.82.0.tgz", + "integrity": "sha512-5meSU8BHFeaT09RWfkuUrikRlC+WZcYb9To7MpfV1d9nlD7CZ2xydPExK+mj3DqRuQvTbvhMPcr7f+pHlgHINQ==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13343,12 +13379,13 @@ } }, "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.80.6.tgz", - "integrity": "sha512-DzeNqU/SN0mWFznoOH4RtVGcrg3Eoa41pUQhKMtrhNbCmIE1zNDunUiAEVTNpdHJF4nxf7ELUPXWmStM31CbUQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.82.0.tgz", + "integrity": "sha512-ASLAMfjWv7YEPBvEOVlb3zzHq8l4Y9Eh4x3m7B1dNauGVbO11Yng5cPCX/XbwGVf30BtE75pwqvV7oXxBtN15w==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13358,12 +13395,13 @@ } }, "node_modules/sass-embedded-linux-riscv64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.80.6.tgz", - "integrity": "sha512-AyoHJ3icV9xuJjq1YzJqpEj2XfiC/KBkVYTUrCELKiXP0DN1gi/BpUwZNCAgCM3CyEdMef4LQM/ztCYJxYzdyg==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.82.0.tgz", + "integrity": "sha512-qWvRDXCXH3GzD8OcP0ntd8gBTK3kZyUeyXmxQDZyEtMAM4STC2Tn7+5+2JYYHlppzqWnZPFBNESvpKeOtHaBBw==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13373,12 +13411,13 @@ } }, "node_modules/sass-embedded-linux-x64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.80.6.tgz", - "integrity": "sha512-EohsE9CEqx0ycylnsEj/0DNPG99Tb0qAVZspiAs5xHFCJjXOFfp3cRQu0BRf+lZ1b72IhPFXymzVtojvzUHb7g==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.82.0.tgz", + "integrity": "sha512-AmRaHqShztwfep+M4NagdGaY7fTyWGSOM3k4Z/dd7q4nZclXbALLqNJtKx8xOM7A41LHYJ9zDpIBVRkrh0PzTA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -13388,12 +13427,13 @@ } }, "node_modules/sass-embedded-win32-arm64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.80.6.tgz", - "integrity": "sha512-29wETQi1ykeVvpd4zMVokpQKFSOZskGJzZawuuNCdo7BHjHKIRDsqbz8YT1CewHPBshI0hfD21fenmjxYjGXPQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.82.0.tgz", + "integrity": "sha512-zL9JDQZHXHSGAZe5DqSrR86wMHbm9QPziU4/3hoIG+99StuS74CuV42+hw/+FXXBkXMWbjKWsyF/HZt+I/wJuw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -13403,12 +13443,13 @@ } }, "node_modules/sass-embedded-win32-ia32": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.80.6.tgz", - "integrity": "sha512-1s3OpK2iTIfIL/a91QhAQnffsbuWfnsM8Lx4Fxt0f7ErnxjCV6q8MUFTV/UhcLtLyTFnPCA62DLjp2KGCjMI9A==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.82.0.tgz", + "integrity": "sha512-xE+AzLquCkFPnnpo0NHjQdLRIhG1bVs42xIKx42aUbVLYKkBDvbBGpw6EtTscRMyvcjoOqGH5saRvSFComUQcw==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -13418,12 +13459,13 @@ } }, "node_modules/sass-embedded-win32-x64": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.80.6.tgz", - "integrity": "sha512-0pH4Zr9silHkcmLPC0ghnD3DI0vMsjA7dKvGR32/RbbjOSvHV5cDQRLiuVJAPp34dfMA7kJd1ysSchRdH0igAQ==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.82.0.tgz", + "integrity": "sha512-cEgfOQG5womOzzk16ReTv2dxPq5BG16LgLUold/LH9IZH86u4E/MN7Fspf4RWeEJ2EcLdew9QYSC2YWs1l98dQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -13445,6 +13487,11 @@ "node": ">=8" } }, + "node_modules/sass-embedded/node_modules/immutable": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.2.tgz", + "integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==" + }, "node_modules/sass-embedded/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -13994,6 +14041,25 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/table": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", diff --git a/package.json b/package.json index ae76a1849..3d61d5b71 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/stream": "^3.0.1", - "@uswds/uswds": "^3.9.0", + "@uswds/uswds": "^3.10.0", "cbor-js": "0.1.0", "d3": "^7.9.0", "govuk_frontend_toolkit": "^9.0.1", @@ -37,10 +37,10 @@ "hogan": "1.0.2", "jquery": "3.7.1", "morphdom": "^2.7.4", - "playwright": "^1.48.2", + "playwright": "^1.49.0", "python": "^0.0.4", "query-command-supported": "1.0.0", - "sass-embedded": "^1.80.6", + "sass-embedded": "^1.82.0", "textarea-caret": "3.1.0", "timeago": "1.6.7", "vinyl-buffer": "^1.0.1", @@ -66,7 +66,7 @@ "jest-environment-jsdom": "^29.2.2", "jshint": "2.13.6", "jshint-stylish": "2.2.1", - "rollup": "^4.24.4", + "rollup": "^4.28.0", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-node-resolve": "5.2.0" } diff --git a/poetry.lock b/poetry.lock index 160d4c831..aff07625d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "ago" @@ -1616,18 +1616,18 @@ files = [ [[package]] name = "moto" -version = "5.0.18" +version = "5.0.22" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.18-py2.py3-none-any.whl", hash = "sha256:8e25401f7d7910e19a732b417e0d503ef86cf4de9114a273dd62679a42f3be1c"}, - {file = "moto-5.0.18.tar.gz", hash = "sha256:8a7ad2f53a2e6cc9db2ff65c0e0d4b5d7e78bc00b825c9e1ff6cc394371e76e9"}, + {file = "moto-5.0.22-py3-none-any.whl", hash = "sha256:defae32e834ba5674f77cbbe996b41dc248dd81289af8032fa3e847284409b29"}, + {file = "moto-5.0.22.tar.gz", hash = "sha256:daf47b8a1f5f190cd3eaa40018a643f38e542277900cf1db7f252cedbfed998f"}, ] [package.dependencies] boto3 = ">=1.9.201" -botocore = ">=1.14.0" +botocore = ">=1.14.0,<1.35.45 || >1.35.45,<1.35.46 || >1.35.46" cryptography = ">=3.3.1" Jinja2 = ">=2.10.1" python-dateutil = ">=2.1,<3.0.0" diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 27e7f074e..e32ca11ea 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -125,6 +125,7 @@ def test_static_pages(client_request, mock_get_organization_by_domain, view, moc "write_for_action", "multiple_languages", "benchmark_performance", + "guidance_index" ] return ( not current_app.config["FEATURE_BEST_PRACTICES_ENABLED"] diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 8185afe82..a7a19b567 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -18,6 +18,7 @@ EXCLUDED_ENDPOINTS = tuple( Navigation.get_endpoint_with_blueprint, { "about_notify", + "about_security", "accept_invite", "accept_org_invite", "accessibility_statement", @@ -219,6 +220,7 @@ EXCLUDED_ENDPOINTS = tuple( "suspend_service", "template_history", "template_usage", + "test_feature_flags", "tour_step", "trial_mode", "trial_mode_new", @@ -258,6 +260,7 @@ EXCLUDED_ENDPOINTS = tuple( "view_template_version", "view_template_versions", "who_its_for", + "why_text_messaging", "write_for_action", }, ) diff --git a/tests/app/utils/test_csv.py b/tests/app/utils/test_csv.py index d603fcd0e..db4b6a0ec 100644 --- a/tests/app/utils/test_csv.py +++ b/tests/app/utils/test_csv.py @@ -58,6 +58,7 @@ def _get_notifications_csv( "to": recipient, "recipient": recipient, "client_reference": "ref 1234", + "carrier": "AT&T Mobility", } for i in range(rows) ], @@ -88,15 +89,15 @@ def get_notifications_csv_mock( ( None, [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", ], ), ( "Anne Example", [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", # noqa + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", # noqa ], ), ], @@ -135,6 +136,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", ], [ "8005555555", @@ -144,6 +146,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", ], ), ( @@ -159,6 +162,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -171,6 +175,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "🐜", "🐝", "🦀", @@ -189,6 +194,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -201,6 +207,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "🐜,🐜", "🐝,🐝", "🦀", diff --git a/tests/end_to_end/test_best_practices_content_pages.py b/tests/end_to_end/test_best_practices_content_pages.py new file mode 100644 index 000000000..5a75f9694 --- /dev/null +++ b/tests/end_to_end/test_best_practices_content_pages.py @@ -0,0 +1,72 @@ +import os +import re + +from playwright.sync_api import expect + +from tests.end_to_end.conftest import check_axe_report + +E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI") + + +def test_best_practices_side_menu(authenticated_page): + page = authenticated_page + + page.goto(f"{E2E_TEST_URI}") + + page.wait_for_load_state("domcontentloaded") + check_axe_report(page) + + response = page.request.get(f"{E2E_TEST_URI}/test/feature-flags") + feature_flags = response.json() + feature_best_practices_enabled = feature_flags.get("FEATURE_BEST_PRACTICES_ENABLED") + + if feature_best_practices_enabled: + page.get_by_role("link", name="Best Practices").click() + expect(page).to_have_title(re.compile("Best Practice")) + + page.get_by_role("link", name="Clear goals", exact=True).click() + expect(page).to_have_title(re.compile("Establish clear goals")) + + page.get_by_role("link", name="Rules and regulations").click() + expect(page).to_have_title(re.compile("Rules and regulations")) + + page.get_by_role("link", name="Establish trust").click() + expect(page).to_have_title(re.compile("Establish trust")) + + page.get_by_role("link", name="Write for action").click() + expect(page).to_have_title(re.compile("Write texts that provoke")) + + page.get_by_role("link", name="Multiple languages").click() + expect(page).to_have_title(re.compile("Text in multiple languages")) + + page.get_by_role("link", name="Benchmark performance").click() + expect(page).to_have_title(re.compile("Measuring performance with")) + + parent_link = page.get_by_role("link", name="Establish trust") + parent_link.hover() + + submenu_item = page.get_by_role("link", name=re.compile("Get the word out")) + submenu_item.click() + + expect(page).to_have_url(re.compile(r"#get-the-word-out")) + + anchor_target = page.locator("#get-the-word-out") + expect(anchor_target).to_be_visible() + anchor_target.click() + + +def test_breadcrumbs_best_practices(authenticated_page): + page = authenticated_page + + page.goto(f"{E2E_TEST_URI}") + + page.wait_for_load_state("domcontentloaded") + check_axe_report(page) + + response = page.request.get(f"{E2E_TEST_URI}/test/feature-flags") + feature_flags = response.json() + feature_best_practices_enabled = feature_flags.get("FEATURE_BEST_PRACTICES_ENABLED") + + if feature_best_practices_enabled: + page.get_by_role("link", name="Clear goals", exact=True).click() + page.locator("ol").get_by_role("link", name="Best Practices").click() diff --git a/urls.js b/urls.js index 0440430ec..bd693cc90 100644 --- a/urls.js +++ b/urls.js @@ -8,29 +8,29 @@ const sublinks = [ { label: 'Trial Mode', path: '/using-notify/trial-mode' }, { label: 'Pricing', path: '/using-notify/pricing' }, { label: 'Delivery Status', path: '/using-notify/delivery-status' }, - { label: 'Guidance', path: '/using-notify/guidance' }, + { label: 'Guidance', path: '/guides/using-notify/guidance' }, { label: 'Features', path: '/features' }, { label: 'Roadmap', path: '/features/roadmap' }, { label: 'Security', path: '/features/security' }, { label: 'Support', path: '/support' }, { label: 'Best Practices', path: '/guides/best-practices' }, - { label: 'Clear Goals', path: '/guides/best-practices/clear-goals' }, + { label: 'Clear Goals', path: '/guides/clear-goals' }, { label: 'Rules And Regulations', - path: '/guides/best-practices/rules-and-regulations', + path: '/guides/rules-and-regulations', }, - { label: 'Establish Trust', path: '/guides/best-practices/establish-trust' }, + { label: 'Establish Trust', path: '/guides/establish-trust' }, { label: 'Write For Action', - path: '/guides/best-practices/write-for-action', + path: '/guides/write-for-action', }, { label: 'Multiple Languages', - path: '/guides/best-practices/multiple-languages', + path: '/guides/multiple-languages', }, { label: 'Benchmark Performance', - path: '/guides/best-practices/benchmark-performance', + path: '/guides/benchmark-performance', }, // Add more links here as needed ];