From 5d625662faf668661d819838d3daf41c53e7a9f4 Mon Sep 17 00:00:00 2001 From: Rechner Fox <659028+rechner@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:27:39 -0700 Subject: [PATCH 1/2] feat(profile): Option to prefer username over full name --- memberportal/access/models.py | 26 +++--- memberportal/api_access/consumers.py | 2 +- memberportal/api_meeting/permissions.py | 17 ++++ memberportal/api_meeting/tests.py | 91 +++++++++++++++++++ memberportal/api_meeting/views.py | 29 +++--- memberportal/api_member_tools/views.py | 17 ++-- memberportal/api_metrics/views.py | 2 +- .../membermatters/constance_config.py | 5 + memberportal/profile/models.py | 11 +++ memberportal/profile/tasks.py | 2 +- .../profile/tests/test_display_name.py | 50 ++++++++++ src-frontend/package-lock.json | 30 +++--- 12 files changed, 231 insertions(+), 51 deletions(-) create mode 100644 memberportal/api_meeting/permissions.py create mode 100644 memberportal/api_meeting/tests.py create mode 100644 memberportal/profile/tests/test_display_name.py diff --git a/memberportal/access/models.py b/memberportal/access/models.py index 00b24c4e..14357ee4 100644 --- a/memberportal/access/models.py +++ b/memberportal/access/models.py @@ -313,9 +313,9 @@ def bump(self, request=None): # notify messaging apps of bump profile = request.user.profile if self.post_to_slack: - post_door_bump_to_slack(profile.get_full_name(), self.name) + post_door_bump_to_slack(profile.get_display_name(), self.name) if self.post_to_discord: - post_door_bump_to_discord(profile.get_full_name(), self.name) + post_door_bump_to_discord(profile.get_display_name(), self.name) self.log_access(request.user.id) request.user.log_event( f"Bumped the {self.name} {self._meta.verbose_name}.", @@ -355,9 +355,11 @@ def log_access(self, member_id, success=True): # TODO replace with generic post_to_messengers if self.post_to_discord: - post_door_swipe_to_discord(profile.get_full_name(), self.name, success) + post_door_swipe_to_discord( + profile.get_display_name(), self.name, success + ) if self.post_to_slack: - post_door_swipe_to_slack(profile.get_full_name(), self.name, success) + post_door_swipe_to_slack(profile.get_display_name(), self.name, success) elif success == "locked_out": metrics.device_access_failures_total.labels( @@ -367,11 +369,11 @@ def log_access(self, member_id, success=True): # TODO replace with generic post_to_messengers if self.post_to_discord: post_door_swipe_to_discord( - profile.get_full_name(), self.name, "locked_out" + profile.get_display_name(), self.name, "locked_out" ) if self.post_to_slack: post_door_swipe_to_slack( - profile.get_full_name(), self.name, "locked_out" + profile.get_display_name(), self.name, "locked_out" ) sms_message = sms.SMS() @@ -385,10 +387,12 @@ def log_access(self, member_id, success=True): # TODO replace with generic post_to_messengers if self.post_to_discord: post_door_swipe_to_discord( - profile.get_full_name(), self.name, "rejected" + profile.get_display_name(), self.name, "rejected" ) if self.post_to_slack: - post_door_swipe_to_slack(profile.get_full_name(), self.name, "rejected") + post_door_swipe_to_slack( + profile.get_display_name(), self.name, "rejected" + ) sms_message = sms.SMS() sms_message.send_inactive_swipe_alert(profile.phone) @@ -437,7 +441,7 @@ def log_access(self, user, log_type="activated"): if self.post_to_discord: post_interlock_swipe_to_discord( - profile.get_full_name(), self.name, type=log_type + profile.get_display_name(), self.name, type=log_type ) if log_type == "activated": @@ -484,7 +488,7 @@ class DoorLog(ExportModelOperationsMixin("door-log"), models.Model): success = models.BooleanField(default=True) def __str__(self): - return f"{self.user.get_full_name()} ({self.user.profile.screen_name}) swiped at {self.door.name} {'successfully' if self.success else 'unsuccessfully'} on {self.date.date()}" + return f"{self.user.profile.get_display_name(include_screen_name=True)} swiped at {self.door.name} {'successfully' if self.success else 'unsuccessfully'} on {self.date.date()}" class InterlockLog(ExportModelOperationsMixin("interlock-log"), models.Model): @@ -510,7 +514,7 @@ class InterlockLog(ExportModelOperationsMixin("interlock-log"), models.Model): total_cost = models.FloatField(default=None, blank=True, null=True) def __str__(self): - return f"{self.user_started.get_full_name()} ({self.user_started.profile.screen_name}) swiped at {self.interlock.name} {'successfully' if self.success else 'unsuccessfully'} for {round(self.total_time.total_seconds() / 60)} mins at {self.date_started.date()}" + return f"{self.user_started.profile.get_display_name(include_screen_name=True)} swiped at {self.interlock.name} {'successfully' if self.success else 'unsuccessfully'} for {round(self.total_time.total_seconds() / 60)} mins at {self.date_started.date()}" def calculate_cost(self): total_cost = self.interlock.cost_per_session diff --git a/memberportal/api_access/consumers.py b/memberportal/api_access/consumers.py index 789f5e76..034b8125 100644 --- a/memberportal/api_access/consumers.py +++ b/memberportal/api_access/consumers.py @@ -594,7 +594,7 @@ def handle_other_packet(self, content): description = f"{product.name} purchased from {self.device.name} ({product.external_id_name})." post_purchase_to_discord( - f"{profile.get_full_name()} ({profile.screen_name}) just bought something from {self.device.name}." + f"{profile.get_display_name(include_screen_name=True)} just bought something from {self.device.name}." ) transaction = MemberBucks() diff --git a/memberportal/api_meeting/permissions.py b/memberportal/api_meeting/permissions.py new file mode 100644 index 00000000..3b6abfb8 --- /dev/null +++ b/memberportal/api_meeting/permissions.py @@ -0,0 +1,17 @@ +from constance import config +from rest_framework import permissions + + +class ProxyVotingPermission(permissions.BasePermission): + """Allow proxy-voting APIs only for the enabled feature's users.""" + + message = "Proxy voting is not available to this user." + + def has_permission(self, request, view): + if not config.ENABLE_PROXY_VOTING or not request.user.is_authenticated: + return False + + profile = getattr(request.user, "profile", None) + return request.user.is_staff or ( + profile is not None and profile.state == "active" + ) diff --git a/memberportal/api_meeting/tests.py b/memberportal/api_meeting/tests.py new file mode 100644 index 00000000..751e9708 --- /dev/null +++ b/memberportal/api_meeting/tests.py @@ -0,0 +1,91 @@ +from constance.test.unittest import override_config +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from profile.models import Profile, User + + +class ProxyVotingEndpointTests(TestCase): + member_endpoints = ( + "/api/tools/meetings/", + "/api/tools/members/", + "/api/proxies/", + ) + proxy_write_endpoints = ( + ("post", "/api/proxies/"), + ("delete", "/api/proxies/999/"), + ) + + def make_user(self, state="active", staff=False): + suffix = User.objects.count() + 1 + user = User.objects.create_user( + f"proxy-endpoint-{suffix}@example.test", + password="test-password", + ) + user.staff = staff + user.save(update_fields=["staff"]) + Profile.objects.create( + user=user, + first_name="Proxy", + last_name=str(suffix), + state=state, + ) + return user + + def assert_gets_status(self, user, expected_status): + client = APIClient() + if user is not None: + client.force_authenticate(user=user) + + for endpoint in self.member_endpoints: + with self.subTest(endpoint=endpoint): + self.assertEqual(client.get(endpoint).status_code, expected_status) + + def test_unauthn_users_cannot_access_proxy_endpoints(self): + with override_config(ENABLE_PROXY_VOTING=True): + self.assert_gets_status(None, status.HTTP_401_UNAUTHORIZED) + + client = APIClient() + for method, endpoint in self.proxy_write_endpoints: + with self.subTest(method=method, endpoint=endpoint): + response = getattr(client, method)(endpoint, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_inactive_members_cannot_access_proxy_endpoints(self): + user = self.make_user(state="inactive") + + with override_config(ENABLE_PROXY_VOTING=True): + self.assert_gets_status(user, status.HTTP_403_FORBIDDEN) + + client = APIClient() + client.force_authenticate(user=user) + for method, endpoint in self.proxy_write_endpoints: + with self.subTest(method=method, endpoint=endpoint): + response = getattr(client, method)(endpoint, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_active_members_can_access_proxy_endpoints(self): + user = self.make_user(state="active") + + with override_config(ENABLE_PROXY_VOTING=True): + self.assert_gets_status(user, status.HTTP_200_OK) + + def test_staff_users_can_access_proxy_endpoints(self): + user = self.make_user(state="inactive", staff=True) + + with override_config(ENABLE_PROXY_VOTING=True): + self.assert_gets_status(user, status.HTTP_200_OK) + + def test_proxy_endpoints_are_disabled_with_the_feature(self): + user = self.make_user(state="active") + + with override_config(ENABLE_PROXY_VOTING=False): + self.assert_gets_status(user, status.HTTP_403_FORBIDDEN) + + client = APIClient() + client.force_authenticate(user=user) + for method, endpoint in self.proxy_write_endpoints: + with self.subTest(method=method, endpoint=endpoint): + response = getattr(client, method)(endpoint, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/memberportal/api_meeting/views.py b/memberportal/api_meeting/views.py index a97eae2b..3b8df407 100644 --- a/memberportal/api_meeting/views.py +++ b/memberportal/api_meeting/views.py @@ -1,4 +1,5 @@ from .models import Meeting, ProxyVote +from .permissions import ProxyVotingPermission from profile.models import Profile from django.utils.timezone import make_aware, localtime from datetime import datetime @@ -23,12 +24,12 @@ def get(self, request): meetings = Meeting.objects.all() def get_attendee(attendee): - return attendee.profile.get_full_name() + return attendee.profile.get_display_name() def get_proxy(proxy): return { - "name": proxy.user.profile.get_full_name(), - "proxyName": proxy.proxy_user.profile.get_full_name(), + "name": proxy.user.profile.get_display_name(), + "proxyName": proxy.proxy_user.profile.get_display_name(), "date": proxy.created_date, } @@ -85,7 +86,7 @@ class Proxies(APIView): delete: delete an existing proxy vote. """ - permission_classes = [permissions.IsAuthenticated] + permission_classes = [ProxyVotingPermission] def get(self, request): proxies = ProxyVote.objects.filter(user=request.user) @@ -93,7 +94,7 @@ def get(self, request): def get_proxy_details(proxy): return { "id": proxy.id, - "name": proxy.proxy_user.profile.get_full_name(), + "name": proxy.proxy_user.profile.get_display_name(), "date": proxy.meeting.date, "type": proxy.meeting.get_type(), } @@ -127,10 +128,8 @@ def post(self, request): meeting=meeting, ) - subject = ( - f"{request.user.profile.get_full_name()} just assigned you as a proxy" - ) - message = f"{request.user.profile.get_full_name()} just assigned you as a proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}." + subject = f"{request.user.profile.get_display_name()} just assigned you as a proxy" + message = f"{request.user.profile.get_display_name()} just assigned you as a proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}." send_single_email( to_email=proxy_user.email, subject=subject, @@ -141,8 +140,8 @@ def post(self, request): user=request.user, ) - subject = f"{proxy_user.profile.get_full_name()} is confirmed as your proxy for the {meeting.get_type()} meeting" - message = f"{proxy_user.profile.get_full_name()} is confirmed as your proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}. You can manage this proxy from the member portal." + subject = f"{proxy_user.profile.get_display_name()} is confirmed as your proxy for the {meeting.get_type()} meeting" + message = f"{proxy_user.profile.get_display_name()} is confirmed as your proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}. You can manage this proxy from the member portal." send_single_email( to_email=request.user.email, subject=subject, @@ -161,9 +160,9 @@ def delete(self, request, proxy_id): if request.user == proxy.user: proxy.delete() subject = ( - f"{request.user.profile.get_full_name()} just removed you as a proxy" + f"{request.user.profile.get_display_name()} just removed you as a proxy" ) - message = f"{request.user.profile.get_full_name()} just removed you as a proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}." + message = f"{request.user.profile.get_display_name()} just removed you as a proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}." send_single_email( to_email=proxy.proxy_user.email, subject=subject, @@ -174,8 +173,8 @@ def delete(self, request, proxy_id): user=request.user, ) - subject = f"{proxy.proxy_user.profile.get_full_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting" - message = f"{proxy.proxy_user.profile.get_full_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}. You can manage this proxy from the member portal." + subject = f"{proxy.proxy_user.profile.get_display_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting" + message = f"{proxy.proxy_user.profile.get_display_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}. You can manage this proxy from the member portal." send_single_email( to_email=request.user.email, subject=subject, diff --git a/memberportal/api_member_tools/views.py b/memberportal/api_member_tools/views.py index 2c0eb52f..45fc057e 100644 --- a/memberportal/api_member_tools/views.py +++ b/memberportal/api_member_tools/views.py @@ -1,6 +1,7 @@ from access.models import DoorLog, InterlockLog from profile.models import Profile from api_meeting.models import Meeting +from api_meeting.permissions import ProxyVotingPermission from constance import config from services.emails import send_email_to_admin from services import discord @@ -46,7 +47,7 @@ def get(self, request): { "name": door.door.name, "date": door.date, - "user": door.user.profile.get_full_name(), + "user": door.user.profile.get_display_name(), } ) @@ -54,7 +55,7 @@ def get(self, request): user_ended = None if interlock.user_ended: - user_ended = interlock.user_ended.profile.get_full_name() + user_ended = interlock.user_ended.profile.get_display_name() interlocks.append( { @@ -62,7 +63,7 @@ def get(self, request): "sessionStart": interlock.date_started, "sessionEnd": interlock.date_ended, "sessionComplete": True if interlock.date_ended else False, - "userOn": interlock.user_started.profile.get_full_name(), + "userOn": interlock.user_started.profile.get_display_name(), "userOff": user_ended, } ) @@ -95,7 +96,7 @@ def get(self, request): last_seen.append( { "id": member.id, - "user": member.get_full_name(), + "user": member.get_display_name(), "never": False, "date": member.last_seen, } @@ -103,7 +104,7 @@ def get(self, request): else: last_seen.append( - {"id": member.id, "user": member.get_full_name(), "never": True} + {"id": member.id, "user": member.get_display_name(), "never": True} ) return Response(last_seen, status=status.HTTP_200_OK) @@ -331,7 +332,7 @@ class MeetingList(APIView): get: Returns a list of upcoming meetings that a member is entitled to vote at. """ - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (ProxyVotingPermission,) queryset = Meeting.objects.filter(date__gt=timezone.now()) def get(self, request): @@ -354,11 +355,13 @@ class Members(APIView): get: gets a list of all members. """ + permission_classes = (ProxyVotingPermission,) + def get(self, request): def get_member(member): return { "id": member.id, - "name": member.get_full_name(), + "name": member.get_display_name(), "screenName": member.screen_name, } diff --git a/memberportal/api_metrics/views.py b/memberportal/api_metrics/views.py index c37ba839..c636a513 100644 --- a/memberportal/api_metrics/views.py +++ b/memberportal/api_metrics/views.py @@ -29,7 +29,7 @@ def get(self, request): on_site["count"] = members.count() for member in members: - on_site["members"].append(member.user.profile.get_full_name()) + on_site["members"].append(member.user.profile.get_display_name()) statistics["on_site"] = on_site diff --git a/memberportal/membermatters/constance_config.py b/memberportal/membermatters/constance_config.py index dc4016e4..23ec482a 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -409,6 +409,10 @@ False, "Require members to set a public screen name during signup. Off means the screen name field is optional.", ), + "PREFER_SCREEN_NAME_OVER_FULL_NAME": ( + False, + "Prefer a member's public screen name over their full name in member-facing displays when it is set.", + ), "MEMBER_CAN_EDIT_BASIC_DETAILS": ( True, "Allow members to edit their own name, phone, address and similar basic profile fields. Turn off to lock profile editing to admins only.", @@ -632,6 +636,7 @@ "MEMBER_CAN_EDIT_BASIC_DETAILS", "MEMBER_CAN_EDIT_EMAIL", "PROFILE_DEFAULT_PHONE_REGION", + "PREFER_SCREEN_NAME_OVER_FULL_NAME", ), ), ( diff --git a/memberportal/profile/models.py b/memberportal/profile/models.py index 6d1dcf1c..f370f9d3 100644 --- a/memberportal/profile/models.py +++ b/memberportal/profile/models.py @@ -1014,6 +1014,17 @@ def get_logs(self): def get_full_name(self): return self.first_name + " " + self.last_name + def get_display_name(self, include_screen_name=False): + """Return the configured public name for member-facing displays.""" + screen_name = (self.screen_name or "").strip() + if config.PREFER_SCREEN_NAME_OVER_FULL_NAME and screen_name: + return screen_name + + display_name = self.get_full_name() + if include_screen_name and screen_name: + return f"{display_name} ({screen_name})" + return display_name + def get_short_name(self): return self.first_name diff --git a/memberportal/profile/tasks.py b/memberportal/profile/tasks.py index 64d92953..834080c9 100644 --- a/memberportal/profile/tasks.py +++ b/memberportal/profile/tasks.py @@ -84,7 +84,7 @@ def sync_listmonk_member(self, outbox_id): user_id=profile.user_id, profile_id=profile.pk, email=profile.user.email, - name=profile.get_full_name(), + name=profile.get_display_name(), desired_state=outbox.desired_state, ) outbox.remote_subscriber_id = remote_subscriber_id diff --git a/memberportal/profile/tests/test_display_name.py b/memberportal/profile/tests/test_display_name.py new file mode 100644 index 00000000..e2fb60f2 --- /dev/null +++ b/memberportal/profile/tests/test_display_name.py @@ -0,0 +1,50 @@ +from constance.test.unittest import override_config +from django.test import TestCase + +from profile.models import Profile + + +class DisplayNameTests(TestCase): + def make_profile(self, screen_name=None): + return Profile( + first_name="First", + last_name="Last", + screen_name=screen_name, + ) + + def test_uses_full_name_by_default(self): + profile = self.make_profile(screen_name="public-handle") + + with override_config(PREFER_SCREEN_NAME_OVER_FULL_NAME=False): + self.assertEqual(profile.get_display_name(), "First Last") + + def test_prefers_screen_name_when_enabled(self): + profile = self.make_profile(screen_name="public-handle") + + with override_config(PREFER_SCREEN_NAME_OVER_FULL_NAME=True): + self.assertEqual(profile.get_display_name(), "public-handle") + + def test_enabled_preference_falls_back_when_screen_name_is_unset(self): + with override_config(PREFER_SCREEN_NAME_OVER_FULL_NAME=True): + for screen_name in (None, "", " "): + with self.subTest(screen_name=screen_name): + profile = self.make_profile(screen_name=screen_name) + self.assertEqual(profile.get_display_name(), "First Last") + + def test_include_screen_name_preserves_legacy_format_when_preference_disabled(self): + profile = self.make_profile(screen_name="public-handle") + + with override_config(PREFER_SCREEN_NAME_OVER_FULL_NAME=False): + self.assertEqual( + profile.get_display_name(include_screen_name=True), + "First Last (public-handle)", + ) + + def test_include_screen_name_does_not_duplicate_preferred_screen_name(self): + profile = self.make_profile(screen_name="public-handle") + + with override_config(PREFER_SCREEN_NAME_OVER_FULL_NAME=True): + self.assertEqual( + profile.get_display_name(include_screen_name=True), + "public-handle", + ) diff --git a/src-frontend/package-lock.json b/src-frontend/package-lock.json index 93503105..bc8f0217 100644 --- a/src-frontend/package-lock.json +++ b/src-frontend/package-lock.json @@ -4782,9 +4782,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.23", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz", - "integrity": "sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4835,9 +4835,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", - "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -5546,9 +5546,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.427", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", - "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", "dev": true, "license": "ISC" }, @@ -8564,9 +8564,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.55", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", - "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, "license": "MIT", "engines": { @@ -11186,9 +11186,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", - "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { From 53697a86cf0f9dc5db8ad30e57692f554797c930 Mon Sep 17 00:00:00 2001 From: Rechner Fox <659028+rechner@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:13:30 -0700 Subject: [PATCH 2/2] chore(deps): Update frontend dependencies --- src-frontend/package-lock.json | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src-frontend/package-lock.json b/src-frontend/package-lock.json index bc8f0217..4789d0ed 100644 --- a/src-frontend/package-lock.json +++ b/src-frontend/package-lock.json @@ -4782,9 +4782,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", - "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4835,9 +4835,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "dev": true, "funding": [ { @@ -4855,11 +4855,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.20", + "baseline-browser-mapping": "^2.11.23", "caniuse-lite": "^1.0.30001810", - "electron-to-chromium": "^1.5.420", - "node-releases": "^2.0.54", - "update-browserslist-db": "^1.3.2" + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -5546,9 +5546,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.420", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", - "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", "dev": true, "license": "ISC" }, @@ -8564,9 +8564,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", + "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==", "dev": true, "license": "MIT", "engines": { @@ -11186,9 +11186,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ {