Skip to content

chore: reduce direct runtime dependencies - #531

Merged
Blaumaus merged 3 commits into
mainfrom
chore/reduce-deps
May 18, 2026
Merged

chore: reduce direct runtime dependencies#531
Blaumaus merged 3 commits into
mainfrom
chore/reduce-deps

Conversation

@Blaumaus

@Blaumaus Blaumaus commented May 14, 2026

Copy link
Copy Markdown
Member

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Animated streaming indicator in AI chat scroll-to-bottom button.
    • Built-in file download utilities for exports.
  • Improvements

    • Better chat auto-scroll and scroll-pinning during streaming/resizes.
    • More accurate and performant timezone picker UI.
    • More reliable outbound webhook requests and timeout handling.
    • Safer IP-range blacklist matching.
  • Chores

    • Optimized Docker builds and smaller production images.
    • Cleaned up and reclassified dependencies.

Review Change Stack

@Blaumaus Blaumaus self-assigned this May 14, 2026
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f138848f-f416-4841-91aa-1129181f3dbf

📥 Commits

Reviewing files that changed from the base of the PR and between ccaa957 and d7532e8.

📒 Files selected for processing (1)
  • web/app/ui/TimezonePicker.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/app/ui/TimezonePicker.tsx

📝 Walkthrough

Walkthrough

This PR replaces several runtime dependencies with internal utilities, adds a filtered POST helper and download helpers, implements IP-range matchers, refactors AskAI scrolling and timezone option construction, and tightens Docker images and package dependency classifications.

Changes

Backend & Web Modernization

Layer / File(s) Summary
IP Range Checking Implementation
backend/apps/cloud/src/common/ip-range.ts, backend/apps/community/src/common/ip-range.ts
New modules parse IPv4/IPv6 addresses into byte arrays and implement CIDR-style prefix matching, with support for IPv4-mapped IPv6 normalization and bitmasked partial-byte prefix checks.
IP Range Integration in Analytics
backend/apps/cloud/src/analytics/analytics.service.ts, backend/apps/community/src/analytics/analytics.service.ts, backend/package.json
Analytics services wire isIpInRange for IP blacklist checks; ip-range-check is removed from runtime deps and package.json entries are reclassified.
Webhook HTTP Client Utility
backend/apps/cloud/src/notification-channel/dispatchers/http-client.ts
New postWithFilteredAgent utility performs filtered HTTP/HTTPS POST requests with explicit Content-Length, redirect rejection for 3xx with Location, timeout handling, and one-shot promise resolution.
Webhook Service Integration
backend/apps/cloud/src/notification-channel/dispatchers/webhook-channel.service.ts
Webhook send/ping methods replace fetch+useAgent and AbortSignal timeouts with postWithFilteredAgent calls.
Backend Docker & Dependencies Cleanup
backend/Dockerfile, backend/package.json
Dockerfile optimizes build stage with selective npm ci, deploy and dbip sync runs, and npm prune --omit=dev; several build/runtime tools moved to devDependencies and start:prod adjusted.
Web Download Utilities Module
web/app/utils/download.ts
New shared utilities downloadBlob and downloadText encapsulate browser downloads via object URLs and transient anchors.
Web Export Integration
web/app/pages/Project/View/ViewProject.helpers.tsx, web/app/pages/Project/tabs/AskAI/exportHelpers.ts
CSV and Markdown export flows now use downloadBlob/downloadText; file-saver and manual blob/URL code removed.
Chat Scroll Behavior Refactoring
web/app/pages/Project/tabs/AskAI/AskAIView.tsx
Replaces use-stick-to-bottom with useChatScroll that tracks bottom-pinned state and exposes scrollToBottom; updates ScrollToBottomButton to accept isStreaming and shows a streaming indicator.
Timezone Picker Intl.DateTimeFormat Migration
web/app/ui/TimezonePicker.tsx
Replaces spacetime/timezone-soft with Intl.DateTimeFormat helpers to compute timezone offsets/names and refactors option parsing/selection; TimezoneSelectProps.value.label made optional.
Web Docker & Dependencies Cleanup
web/Dockerfile, web/package.json
Final web image copies only essential artifacts and moves dev-only packages to devDependencies (removes file-saver, use-stick-to-bottom, spacetime, etc.).
Type-Only Imports Cleanup
web/app/pages/Project/View/components/InteractiveMap.tsx
Converts geojson Feature and GeoJsonObject imports to type-only imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Swetrix/swetrix#510: touches analytics funnel/output code and may overlap surface areas for analytics changes.

Poem

🐰 I stitched IP ranges, trimmed deps tight,

Webhooks now post with a filtered light,
Downloads breeze out with a single click,
Chat stays grounded while messages flick,
Timezones sing neat — the rabbit hops in delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description follows the template structure but provides only checkbox responses without explaining what specific dependency changes were made or why they were removed. Add a 'Changes' section describing which dependencies were removed/replaced, why they were changed, and how the new implementations work (e.g., IP range checking, HTTP utilities, timezone formatting).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of the PR, which is to reduce direct runtime dependencies across both backend and web packages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/reduce-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/app/pages/Project/tabs/AskAI/AskAIView.tsx (1)

989-1014: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ScrollToBottomButton becomes ambiguous while streaming.

When isStreaming is true and the user is not at the bottom, the button renders three bouncing dots with aria-label = "thinking". The button is still wired to scrollToBottom, but visually and semantically it now looks like a thinking indicator rather than a scroll affordance — users (and screen-reader users especially, given the aria-label) have no cue that clicking it scrolls them down. Consider keeping the arrow as the primary visual (perhaps overlaid with a small loading dot), and keeping the aria-label as "scroll to bottom" regardless of streaming state, so the click affordance and accessibility semantics are preserved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/pages/Project/tabs/AskAI/AskAIView.tsx` around lines 989 - 1014, The
button currently shows bouncing dots and sets aria-label to "thinking" when
isStreaming, which hides the scroll affordance; change the rendering so the
ArrowDownIcon (ArrowDownIcon) remains the primary visible element and the
onClick handler scrollToBottom stays attached, and when isStreaming render a
small overlay/supplemental loading indicator (e.g., tiny dots or spinner) next
to/over the arrow rather than replacing it; also always set aria-label to
t('project.askAi.scrollToBottom') (do not switch to t('project.askAi.thinking'))
so screen readers and users retain the scroll affordance while still indicating
background streaming state.
🧹 Nitpick comments (4)
backend/apps/cloud/src/common/ip-range.ts (1)

1-148: ⚡ Quick win

Add unit tests for the new IP range matcher.

This module replaces a battle-tested external library on a security-relevant path (analytics IP blacklist). Worth adding focused unit tests covering at least:

  • IPv4 exact match and /0, /8, /24, /32 prefixes (including boundary bits inside a non-byte-aligned prefix).
  • IPv6 full form, :: compression at start/middle/end, and :: alone.
  • IPv4-mapped IPv6 (::ffff:a.b.c.d) matched against both IPv4 and IPv6 forms.
  • Malformed inputs: invalid octets (256.0.0.1), leading zeros, multiple ::, non-numeric prefix, prefix out of range.
  • Array vs string ranges input.

This catches regressions cheaply now that the implementation is in-house.

#!/bin/bash
# Confirm no existing tests cover the new module.
fd -t f 'ip-range' 
rg -nP --type=ts -C2 "from ['\"].*common/ip-range['\"]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/common/ip-range.ts` around lines 1 - 148, Add
comprehensive unit tests for the new IP range matcher by exercising the public
isIpInRange(address, ranges) function and targeting edge cases in
parseIp/parseIPv4/parseIPv6Bytes behavior: create a test file that asserts IPv4
exact matches and prefixes (/0, /8, /24, /32 and non-byte-aligned prefixes),
IPv6 full and compressed forms (start/middle/end compression and "::" alone),
IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) matched against both IPv4 and IPv6
ranges, malformed inputs (e.g. 256.0.0.1, leading zeros, multiple "::",
non-numeric prefix, out-of-range prefix), and both string and array forms of the
ranges parameter; use matchesSingleRange/matchesPrefix indirectly via
isIpInRange to validate expected true/false outcomes for each case.
web/app/pages/Project/tabs/AskAI/AskAIView.tsx (2)

1053-1063: 💤 Low value

Scroll listener won't re-attach if scrollRef's element remounts.

useEffect depends only on updateIsAtBottom (stable), so the listener is bound exactly once on first commit using scrollRef.current captured at that time. In this file the ref={scrollRef} div (around line 3033) stays mounted across the empty/active states, so this is fine today — but if a future refactor ever conditionally renders the scroll container (e.g., separate empty-state and chat layouts), isAtBottom will silently stop updating. Consider either documenting that invariant or attaching the listener via a callback ref so it follows whichever element holds it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/pages/Project/tabs/AskAI/AskAIView.tsx` around lines 1053 - 1063, The
effect that attaches the scroll listener uses scrollRef.current captured once
and only depends on updateIsAtBottom, so if the DOM element ever
unmounts/remounts the listener won't reattach; change the implementation to
attach/detach the listener to the current element when it mounts by using a
callback ref or by including scrollRef.current in the effect dependencies:
update the code around useEffect / scrollRef to either replace the ref with a
callback ref that calls addEventListener on the new element and removes it from
the previous one (ensuring passive: true) or modify the useEffect to read
scrollRef.current inside and re-run when the ref value changes, referencing
symbols scrollRef and updateIsAtBottom and keeping the cleanup to
removeEventListener to avoid leaks.

2859-2874: 💤 Low value

Auto-scroll effect: isAtBottomRef in deps is a no-op; behavior relies on incidental state updates.

isAtBottomRef is a ref object whose identity never changes, so listing it in the deps array doesn't cause re-runs. The effect actually re-runs because messages, streamingMessage, and isWaitingForResponse change. That happens to be sufficient today (streaming updates streamingMessage on every chunk), but it ties scroll-pinning correctness to the assumption that every content-size change is accompanied by one of those state updates. If a future change adds content that grows the scroll container without bumping these states (e.g., async-rendered chart, image load, expanded reasoning block), the user will silently drift off-bottom. A ResizeObserver on contentRef would be a more direct trigger.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/pages/Project/tabs/AskAI/AskAIView.tsx` around lines 2859 - 2874, The
effect that auto-scrolls (currently watching messages, streamingMessage,
isWaitingForResponse, isChatActive, isAtBottomRef, scrollToBottom) incorrectly
relies on isAtBottomRef in deps and misses content-size only changes; replace or
augment this approach by hooking a ResizeObserver to the chat content element
(contentRef) and call scrollToBottom('auto') when the observer detects size
changes while isChatActive and isAtBottomRef.current are true. Keep the existing
state-based triggers (messages/streamingMessage/isWaitingForResponse) but add
the ResizeObserver setup/cleanup inside the same useEffect (or a new effect) so
that the observer is attached to contentRef.current and is disconnected on
cleanup, ensuring scrollToBottom is invoked on layout/size changes that don’t
emit state updates.
web/app/ui/TimezonePicker.tsx (1)

11-42: 💤 Low value

Minor numeric precision in offset computation.

asUtc is computed at second granularity (no ms), while date.getTime() includes milliseconds. The subtraction therefore carries up to ~999 ms of jitter from date.getMilliseconds() before Math.round((... ) / 60000). For every standard timezone (offsets at 15-minute boundaries) the rounding hides it, so this is effectively safe today — just flagging it because if anyone later changes the rounding to Math.trunc or compares offsets exactly, the ms component can flip the result by 1 minute. Zeroing out date's ms once at the top of the function would make the math exact:

-    return Math.round((asUtc - date.getTime()) / 60000)
+    const baseMs = Math.floor(date.getTime() / 1000) * 1000
+    return Math.round((asUtc - baseMs) / 60000)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/ui/TimezonePicker.tsx` around lines 11 - 42, getTimezoneOffsetMinutes
can return an offset off-by-one minute due to date containing milliseconds while
asUtc is built to second precision; fix by zeroing out the milliseconds on the
Date instance used (the local "date" referenced inside getTimezoneOffsetMinutes)
at the start of the function (e.g., set date.setMilliseconds(0) or create a new
Date with ms cleared) so the subtraction between asUtc and date.getTime() is
exact to the second before Math.round is applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/ui/TimezonePicker.tsx`:
- Line 9: The module-level const date and precomputed options freeze timezone
offsets/abbreviations at import time causing stale DST data; fix by making the
options computation lazy and time-aware: move the date/new-options logic
(currently using date and options) into the TimezoneSelect/TimezonePicker
component (or a buildOptions function) and compute it on demand—e.g. inside the
component or when the picker opens—memoizing with useMemo or recomputing when
the picker is shown so offsets/abbrev/altName reflect the current Date; update
references to date and options accordingly.

In `@web/app/utils/download.ts`:
- Line 9: The object URL is revoked immediately with setTimeout(..., 0), which
can cancel downloads before they start; change the revocation to use a short
delay (e.g., 100–200ms, recommended ~150ms) so the browser has time to begin the
download before calling URL.revokeObjectURL(url) — locate the line with
setTimeout(() => URL.revokeObjectURL(url), 0) in web/app/utils/download.ts and
replace the 0 delay with a 150 (or configurable) millisecond delay.

In `@web/Dockerfile`:
- Around line 48-49: The Dockerfile currently copies start.cjs (used by
ecosystem.config.cjs via script: './start.cjs') but the image CMD is
["npm","run","start"] which runs react-router-serve ./build/server/index.js
directly and bypasses start.cjs/.env loading; either (A) add a brief comment
above the COPY explaining the image is intended for PM2/self-hosted deployments
and keep COPY for ecosystem.config.cjs, or (B) if the image should run
standalone, remove the COPY and update CMD to invoke start.cjs so .env is loaded
in-process, or (C) keep current CMD and remove COPY but document that .env must
be injected by Docker runtime—choose one and apply consistently (referencing
start.cjs, ecosystem.config.cjs, and the Docker CMD).

---

Outside diff comments:
In `@web/app/pages/Project/tabs/AskAI/AskAIView.tsx`:
- Around line 989-1014: The button currently shows bouncing dots and sets
aria-label to "thinking" when isStreaming, which hides the scroll affordance;
change the rendering so the ArrowDownIcon (ArrowDownIcon) remains the primary
visible element and the onClick handler scrollToBottom stays attached, and when
isStreaming render a small overlay/supplemental loading indicator (e.g., tiny
dots or spinner) next to/over the arrow rather than replacing it; also always
set aria-label to t('project.askAi.scrollToBottom') (do not switch to
t('project.askAi.thinking')) so screen readers and users retain the scroll
affordance while still indicating background streaming state.

---

Nitpick comments:
In `@backend/apps/cloud/src/common/ip-range.ts`:
- Around line 1-148: Add comprehensive unit tests for the new IP range matcher
by exercising the public isIpInRange(address, ranges) function and targeting
edge cases in parseIp/parseIPv4/parseIPv6Bytes behavior: create a test file that
asserts IPv4 exact matches and prefixes (/0, /8, /24, /32 and non-byte-aligned
prefixes), IPv6 full and compressed forms (start/middle/end compression and "::"
alone), IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) matched against both IPv4
and IPv6 ranges, malformed inputs (e.g. 256.0.0.1, leading zeros, multiple "::",
non-numeric prefix, out-of-range prefix), and both string and array forms of the
ranges parameter; use matchesSingleRange/matchesPrefix indirectly via
isIpInRange to validate expected true/false outcomes for each case.

In `@web/app/pages/Project/tabs/AskAI/AskAIView.tsx`:
- Around line 1053-1063: The effect that attaches the scroll listener uses
scrollRef.current captured once and only depends on updateIsAtBottom, so if the
DOM element ever unmounts/remounts the listener won't reattach; change the
implementation to attach/detach the listener to the current element when it
mounts by using a callback ref or by including scrollRef.current in the effect
dependencies: update the code around useEffect / scrollRef to either replace the
ref with a callback ref that calls addEventListener on the new element and
removes it from the previous one (ensuring passive: true) or modify the
useEffect to read scrollRef.current inside and re-run when the ref value
changes, referencing symbols scrollRef and updateIsAtBottom and keeping the
cleanup to removeEventListener to avoid leaks.
- Around line 2859-2874: The effect that auto-scrolls (currently watching
messages, streamingMessage, isWaitingForResponse, isChatActive, isAtBottomRef,
scrollToBottom) incorrectly relies on isAtBottomRef in deps and misses
content-size only changes; replace or augment this approach by hooking a
ResizeObserver to the chat content element (contentRef) and call
scrollToBottom('auto') when the observer detects size changes while isChatActive
and isAtBottomRef.current are true. Keep the existing state-based triggers
(messages/streamingMessage/isWaitingForResponse) but add the ResizeObserver
setup/cleanup inside the same useEffect (or a new effect) so that the observer
is attached to contentRef.current and is disconnected on cleanup, ensuring
scrollToBottom is invoked on layout/size changes that don’t emit state updates.

In `@web/app/ui/TimezonePicker.tsx`:
- Around line 11-42: getTimezoneOffsetMinutes can return an offset off-by-one
minute due to date containing milliseconds while asUtc is built to second
precision; fix by zeroing out the milliseconds on the Date instance used (the
local "date" referenced inside getTimezoneOffsetMinutes) at the start of the
function (e.g., set date.setMilliseconds(0) or create a new Date with ms
cleared) so the subtraction between asUtc and date.getTime() is exact to the
second before Math.round is applied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be16b354-abc3-4ceb-a454-a97bd9e54f6b

📥 Commits

Reviewing files that changed from the base of the PR and between 29f8a29 and 8f41cb8.

⛔ Files ignored due to path filters (2)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • backend/Dockerfile
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • backend/apps/cloud/src/common/ip-range.ts
  • backend/apps/cloud/src/notification-channel/dispatchers/http-client.ts
  • backend/apps/cloud/src/notification-channel/dispatchers/webhook-channel.service.ts
  • backend/apps/community/src/analytics/analytics.service.ts
  • backend/apps/community/src/common/ip-range.ts
  • backend/package.json
  • web/Dockerfile
  • web/app/pages/Project/View/ViewProject.helpers.tsx
  • web/app/pages/Project/View/components/InteractiveMap.tsx
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • web/app/pages/Project/tabs/AskAI/exportHelpers.ts
  • web/app/ui/TimezonePicker.tsx
  • web/app/utils/download.ts
  • web/package.json

Comment thread web/app/ui/TimezonePicker.tsx Outdated
Comment thread web/app/utils/download.ts Outdated
Comment thread web/Dockerfile Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/ui/TimezonePicker.tsx`:
- Around line 81-114: buildOptions is being called unconditionally inside
TimezoneSelect causing expensive Intl calls on every render; wrap the options
generation in a useMemo inside TimezoneSelect so the heavy work
(buildOptions/getTimezoneName/getTimezoneOffsetMinutes/formatOffset) only runs
when needed. Compute a stable cache key that invalidates daily (e.g., derive a
day-based string from new Date() or timezone-sensitive epoch) and pass that as a
dependency to useMemo so DST changes refresh the list; replace direct call to
buildOptions() with the memoized result and keep buildOptions unchanged as the
pure generator function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ebfce10-b694-43a2-81ea-1b0a90a4b10b

📥 Commits

Reviewing files that changed from the base of the PR and between 8f41cb8 and ccaa957.

📒 Files selected for processing (5)
  • backend/apps/cloud/src/common/ip-range.ts
  • web/Dockerfile
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • web/app/ui/TimezonePicker.tsx
  • web/app/utils/download.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/app/utils/download.ts
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • backend/apps/cloud/src/common/ip-range.ts
  • web/Dockerfile

Comment thread web/app/ui/TimezonePicker.tsx
@Blaumaus
Blaumaus merged commit 8429ff8 into main May 18, 2026
12 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 15, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant