Compare commits

...

4 Commits

Author SHA1 Message Date
Giteabot
544450a212 fix some file icon ui (#36078) (#36088)
Backport #36078 by @a1012112796

fix #36071

looks that's because if an svg in hiden env, it's color added by
`fill="url(#a)"` will become not usefull. by ai helping, I think moving
it out of page by position is a good solution. fell free creat a new
pull request if you have a better soluton. Thanks.
<img width="2198" height="1120" alt="image"
src="https://github.com/user-attachments/assets/bbf7c171-0b7f-412a-a1bc-aea3f1629636"
/>

Signed-off-by: a1012112796 <1012112796@qq.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-04 19:39:35 +00:00
Giteabot
0ab447005d Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
Backport #36055 by @lunny

Replace #36032
Fix #36030

This PR use `net/smtp` instead of gomail's smtp. Now
github.com/wneessen/go-mail will be used only for generating email
message body.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-04 18:58:01 +00:00
Giteabot
52902d4ece Fix edit user email bug in API (#36068) (#36081)
Backport #36068 by @lunny

Follow #36058 for API edit user bug when editing email.

- The Admin Edit User API includes a breaking change. Previously, when
updating a user with an email from an unallowed domain, the request
would succeed but return a warning in the response headers. Now, the
request will fail and return an error in the response body instead.
- Removed `AdminAddOrSetPrimaryEmailAddress` because it will not be used
any where.

Fix https://github.com/go-gitea/gitea/pull/36058#issuecomment-3600005186

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-04 18:25:52 +00:00
silverwind
0e91c8a068 Bump toolchain to go1.25.5, misc fixes (#36082)
Backport toolchain change into 1.25. This is needed because of the
govulncheck issue
[present](https://github.com/go-gitea/gitea/actions/runs/19921920886/job/57112316941)
in the branch.

---------

Signed-off-by: silverwind <me@silverwind.io>
2025-12-04 17:57:59 +00:00
23 changed files with 115 additions and 160 deletions

View File

@@ -11,7 +11,7 @@ jobs:
if: github.repository == 'go-gitea/gitea'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -101,7 +101,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -116,7 +116,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -145,7 +145,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -190,7 +190,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -39,7 +39,7 @@ jobs:
- "9000:9000"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -125,7 +125,7 @@ jobs:
- 10000:10000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -178,7 +178,7 @@ jobs:
- "993:993"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -218,7 +218,7 @@ jobs:
- 10000:10000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -16,7 +16,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -65,7 +65,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -107,7 +107,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -17,7 +17,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -21,7 +21,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true

View File

@@ -35,7 +35,7 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf0
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.8
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0

4
go.mod
View File

@@ -1,6 +1,8 @@
module code.gitea.io/gitea
go 1.25.4
go 1.25.0
toolchain go1.25.5
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:

View File

@@ -76,7 +76,7 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = svgHTML
}
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
return template.HTML(`<svg ` + svgCommonAttrs + `><use href="#` + svgID + `"></use></svg>`)
}
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {

View File

@@ -25,7 +25,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return ""
}
sb := &strings.Builder{}
sb.WriteString(`<div class=tw-hidden>`)
sb.WriteString(`<div class="svg-icon-container">`)
for _, icon := range p.IconSVGs {
sb.WriteString(string(icon))
}

View File

@@ -216,9 +216,12 @@ func EditUser(ctx *context.APIContext) {
}
if form.Email != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
@@ -227,10 +230,6 @@ func EditUser(ctx *context.APIContext) {
}
return
}
if !user_model.IsEmailDomainAllowed(*form.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
}
}
opts := &user_service.UpdateOptions{

View File

@@ -33,7 +33,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
})
mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{

View File

@@ -33,7 +33,13 @@ func (s *SendmailSender) Send(from string, to []string, msg io.WriterTo) error {
args := []string{"-f", envelopeFrom, "-i"}
args = append(args, setting.MailService.SendmailArgs...)
args = append(args, to...)
for _, recipient := range to {
smtpTo, err := sanitizeEmailAddress(recipient)
if err != nil {
return fmt.Errorf("invalid recipient address %q: %w", recipient, err)
}
args = append(args, smtpTo)
}
log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args)
desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args)

View File

@@ -9,13 +9,13 @@ import (
"fmt"
"io"
"net"
"net/mail"
"net/smtp"
"os"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/wneessen/go-mail/smtp"
)
// SMTPSender Sender SMTP mail sender
@@ -108,7 +108,7 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
if strings.Contains(options, "CRAM-MD5") {
auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd)
} else if strings.Contains(options, "PLAIN") {
auth = smtp.PlainAuth("", opts.User, opts.Passwd, host, false)
auth = smtp.PlainAuth("", opts.User, opts.Passwd, host)
} else if strings.Contains(options, "LOGIN") {
// Patch for AUTH LOGIN
auth = LoginAuth(opts.User, opts.Passwd)
@@ -123,18 +123,24 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
}
}
if opts.OverrideEnvelopeFrom {
if err = client.Mail(opts.EnvelopeFrom); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
} else {
if err = client.Mail(fmt.Sprintf("<%s>", from)); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
fromAddr := from
if opts.OverrideEnvelopeFrom && opts.EnvelopeFrom != "" {
fromAddr = opts.EnvelopeFrom
}
smtpFrom, err := sanitizeEmailAddress(fromAddr)
if err != nil {
return fmt.Errorf("invalid envelope from address: %w", err)
}
if err = client.Mail(smtpFrom); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
for _, rec := range to {
if err = client.Rcpt(rec); err != nil {
smtpTo, err := sanitizeEmailAddress(rec)
if err != nil {
return fmt.Errorf("invalid recipient address %q: %w", rec, err)
}
if err = client.Rcpt(smtpTo); err != nil {
return fmt.Errorf("failed to issue RCPT command: %w", err)
}
}
@@ -155,3 +161,11 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
return nil
}
func sanitizeEmailAddress(raw string) (string, error) {
addr, err := mail.ParseAddress(strings.TrimSpace(strings.Trim(raw, "<>")))
if err != nil {
return "", err
}
return addr.Address, nil
}

View File

@@ -6,9 +6,9 @@ package sender
import (
"errors"
"fmt"
"net/smtp"
"github.com/Azure/go-ntlmssp"
"github.com/wneessen/go-mail/smtp"
)
type loginAuth struct {

View File

@@ -0,0 +1,30 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package sender
import "testing"
func TestSanitizeEmailAddress(t *testing.T) {
tests := []struct {
input string
expected string
hasError bool
}{
{"abc@gitea.com", "abc@gitea.com", false},
{"<abc@gitea.com>", "abc@gitea.com", false},
{"ssss.com", "", true},
{"<invalid-email>", "", true},
}
for _, tt := range tests {
result, err := sanitizeEmailAddress(tt.input)
if (err != nil) != tt.hasError {
t.Errorf("sanitizeEmailAddress(%q) unexpected error status: got %v, want error: %v", tt.input, err != nil, tt.hasError)
continue
}
if result != tt.expected {
t.Errorf("sanitizeEmailAddress(%q) = %q; want %q", tt.input, result, tt.expected)
}
}
}

View File

@@ -67,13 +67,13 @@ func TestGetTreeViewNodes(t *testing.T) {
curRepoLink := "/any/repo-link"
renderedIconPool := fileicon.NewRenderedIconPool()
mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
mockIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
mockOpenIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "")
assert.NoError(t, err)

View File

@@ -14,60 +14,6 @@ import (
"code.gitea.io/gitea/modules/util"
)
// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address
func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
if strings.EqualFold(u.Email, emailStr) {
return nil
}
if err := user_model.ValidateEmailForAdmin(emailStr); err != nil {
return err
}
// Check if address exists already
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
if email != nil && email.UID != u.ID {
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
}
// Update old primary address
primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
if err != nil {
return err
}
primary.IsPrimary = false
if err := user_model.UpdateEmailAddress(ctx, primary); err != nil {
return err
}
// Insert new or update existing address
if email != nil {
email.IsPrimary = true
email.IsActivated = true
if err := user_model.UpdateEmailAddress(ctx, email); err != nil {
return err
}
} else {
email = &user_model.EmailAddress{
UID: u.ID,
Email: emailStr,
IsActivated: true,
IsPrimary: true,
}
if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
return err
}
}
u.Email = emailStr
return user_model.UpdateUserCols(ctx, u, "email")
}
func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
if strings.EqualFold(u.Email, emailStr) {
return nil

View File

@@ -9,61 +9,10 @@ import (
organization_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27})
emails, err := user_model.GetEmailAddresses(t.Context(), user.ID)
assert.NoError(t, err)
assert.Len(t, emails, 1)
primary, err := user_model.GetPrimaryEmailAddressOfUser(t.Context(), user.ID)
assert.NoError(t, err)
assert.NotEqual(t, "new-primary@example.com", primary.Email)
assert.Equal(t, user.Email, primary.Email)
assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(t.Context(), user, "new-primary@example.com"))
primary, err = user_model.GetPrimaryEmailAddressOfUser(t.Context(), user.ID)
assert.NoError(t, err)
assert.Equal(t, "new-primary@example.com", primary.Email)
assert.Equal(t, user.Email, primary.Email)
emails, err = user_model.GetEmailAddresses(t.Context(), user.ID)
assert.NoError(t, err)
assert.Len(t, emails, 2)
setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
defer func() {
setting.Service.EmailDomainAllowList = []glob.Glob{}
}()
assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(t.Context(), user, "new-primary2@example2.com"))
primary, err = user_model.GetPrimaryEmailAddressOfUser(t.Context(), user.ID)
assert.NoError(t, err)
assert.Equal(t, "new-primary2@example2.com", primary.Email)
assert.Equal(t, user.Email, primary.Email)
assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(t.Context(), user, "user27@example.com"))
primary, err = user_model.GetPrimaryEmailAddressOfUser(t.Context(), user.ID)
assert.NoError(t, err)
assert.Equal(t, "user27@example.com", primary.Email)
assert.Equal(t, user.Email, primary.Email)
emails, err = user_model.GetEmailAddresses(t.Context(), user.ID)
assert.NoError(t, err)
assert.Len(t, emails, 3)
}
func TestReplacePrimaryEmailAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@@ -382,10 +382,12 @@ func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
SourceID: 0,
Email: &newEmail,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
resp := MakeRequest(t, req, http.StatusBadRequest)
errMap := make(map[string]string)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &errMap))
assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", errMap["message"])
originalEmail := "user2@example.com"
originalEmail := "user2@example.org"
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
LoginName: "user2",
SourceID: 0,

View File

@@ -1,17 +1,24 @@
.svg {
/* some material icons have "fill=none" (e.g.: ".txt -> document"), so the CSS styles shouldn't overwrite it,
and material icons should have no "fill" set explicitly, otherwise some like ".editorconfig" won't render correctly */
.svg:not(.git-entry-icon) {
display: inline-block;
vertical-align: text-top;
fill: currentcolor;
}
.svg.git-entry-icon {
fill: transparent; /* some material icons have dark background fill, so need to reset */
}
.middle .svg {
vertical-align: middle;
}
/* some browsers like Chrome have a bug: when a SVG is in a "display: none" container and referenced
somewhere else by `<use href="#id">`, it won't be rendered correctly. e.g.: ".kts -> kotlin" */
.svg-icon-container {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
/* prevent SVGs from shrinking, like in space-starved flexboxes. the sizes
here are cherry-picked for our use cases, feel free to add more. after
https://developer.mozilla.org/en-US/docs/Web/CSS/attr#type-or-unit is

View File

@@ -17,7 +17,7 @@ export function createViewFileTreeStore(props: {repoLink: string, treePath: stri
if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
}
if (poolSvgs.length) {
const svgContainer = createElementFromHTML(html`<div class="global-svg-icon-pool tw-hidden"></div>`);
const svgContainer = createElementFromHTML(html`<div class="global-svg-icon-pool svg-icon-container"></div>`);
svgContainer.innerHTML = poolSvgs.join('');
document.body.append(svgContainer);
}