diff --git a/README.md b/README.md index 32421d883..dd248aea5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ OpenReplay can be deployed anywhere. Follow our step-by-step guides for deployin ## OpenReplay Cloud -For those who want to simply use OpenReplay as a service, [sign up](https://asayer.io/register.html) for a free account on our cloud offering. +For those who want to simply use OpenReplay as a service, [sign up](https://app.openreplay.com/signup) for a free account on our cloud offering. ## Community Support diff --git a/api/.chalice/config.json b/api/.chalice/config.json index c3bd72cb6..b6f821e3a 100644 --- a/api/.chalice/config.json +++ b/api/.chalice/config.json @@ -33,7 +33,7 @@ "sourcemaps_reader": "http://utilities-openreplay.app.svc.cluster.local:9000/sourcemaps", "sourcemaps_bucket": "sourcemaps", "js_cache_bucket": "sessions-assets", - "peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers", + "peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers", "async_Token": "", "EMAIL_HOST": "", "EMAIL_PORT": "587", diff --git a/api/chalicelib/core/assist.py b/api/chalicelib/core/assist.py index 4b57145e8..dbfe8269b 100644 --- a/api/chalicelib/core/assist.py +++ b/api/chalicelib/core/assist.py @@ -21,7 +21,7 @@ SESSION_PROJECTION_COLS = """s.project_id, def get_live_sessions(project_id, filters=None): project_key = projects.get_project_key(project_id) - connected_peers = requests.get(environ["peers"] + f"/{project_key}") + connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}") if connected_peers.status_code != 200: print("!! issue with the peer-server") print(connected_peers.text) @@ -65,7 +65,7 @@ def get_live_sessions(project_id, filters=None): def is_live(project_id, session_id, project_key=None): if project_key is None: project_key = projects.get_project_key(project_id) - connected_peers = requests.get(environ["peers"] + f"/{project_key}") + connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}") if connected_peers.status_code != 200: print("!! issue with the peer-server") print(connected_peers.text) diff --git a/api/chalicelib/core/weekly_report.py b/api/chalicelib/core/weekly_report.py index 23ff97446..e0e6e0fa5 100644 --- a/api/chalicelib/core/weekly_report.py +++ b/api/chalicelib/core/weekly_report.py @@ -1,4 +1,5 @@ from chalicelib.utils import pg_client, helper +from chalicelib.utils.TimeUTC import TimeUTC from chalicelib.utils.helper import environ from chalicelib.utils.helper import get_issue_title @@ -30,7 +31,11 @@ def edit_config(user_id, weekly_report): def cron(): with pg_client.PostgresClient() as cur: - cur.execute("""\ + params = {"3_days_ago": TimeUTC.midnight(delta_days=-3), + "1_week_ago": TimeUTC.midnight(delta_days=-7), + "2_week_ago": TimeUTC.midnight(delta_days=-14), + "5_week_ago": TimeUTC.midnight(delta_days=-35)} + cur.execute(cur.mogrify("""\ SELECT project_id, name AS project_name, users.emails AS emails, @@ -44,7 +49,7 @@ def cron(): SELECT sessions.project_id FROM public.sessions WHERE sessions.project_id = projects.project_id - AND start_ts >= (EXTRACT(EPOCH FROM now() - INTERVAL '3 days') * 1000)::BIGINT + AND start_ts >= %(3_days_ago)s LIMIT 1) AS recently_active USING (project_id) INNER JOIN LATERAL ( SELECT COALESCE(ARRAY_AGG(email), '{}') AS emails @@ -54,14 +59,14 @@ def cron(): AND users.weekly_report ) AS users ON (TRUE) LEFT JOIN LATERAL ( - SELECT COUNT(issues.*) AS count + SELECT COUNT(1) AS count FROM events_common.issues INNER JOIN public.sessions USING (session_id) WHERE sessions.project_id = projects.project_id AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT ) AS week_0_issues ON (TRUE) LEFT JOIN LATERAL ( - SELECT COUNT(issues.*) AS count + SELECT COUNT(1) AS count FROM events_common.issues INNER JOIN public.sessions USING (session_id) WHERE sessions.project_id = projects.project_id @@ -69,16 +74,17 @@ def cron(): AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '2 week') * 1000)::BIGINT ) AS week_1_issues ON (TRUE) LEFT JOIN LATERAL ( - SELECT COUNT(issues.*) AS count + SELECT COUNT(1) AS count FROM events_common.issues INNER JOIN public.sessions USING (session_id) WHERE sessions.project_id = projects.project_id AND issues.timestamp <= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '5 week') * 1000)::BIGINT ) AS month_1_issues ON (TRUE) - WHERE projects.deleted_at ISNULL;""") + WHERE projects.deleted_at ISNULL;"""), params) projects_data = cur.fetchall() for p in projects_data: + params["project_id"] = p["project_id"] print(f"checking {p['project_name']} : {p['project_id']}") if len(p["emails"]) == 0 \ or p["this_week_issues_count"] + p["past_week_issues_count"] + p["past_month_issues_count"] == 0: @@ -104,7 +110,7 @@ def cron(): DATE_TRUNC('day', now()) - INTERVAL '1 day', '1 day'::INTERVAL ) AS timestamp_i - ORDER BY timestamp_i;""", {"project_id": p["project_id"]})) + ORDER BY timestamp_i;""", params)) days_partition = cur.fetchall() max_days_partition = max(x['issues_count'] for x in days_partition) for d in days_partition: @@ -120,7 +126,7 @@ def cron(): AND timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '7 days') * 1000)::BIGINT GROUP BY type ORDER BY count DESC, type - LIMIT 4;""", {"project_id": p["project_id"]})) + LIMIT 4;""", params)) issues_by_type = cur.fetchall() max_issues_by_type = sum(i["count"] for i in issues_by_type) for i in issues_by_type: @@ -149,7 +155,7 @@ def cron(): '1 day'::INTERVAL ) AS timestamp_i GROUP BY timestamp_i - ORDER BY timestamp_i;""", {"project_id": p["project_id"]})) + ORDER BY timestamp_i;""", params)) issues_breakdown_by_day = cur.fetchall() for i in issues_breakdown_by_day: i["sum"] = sum(x["count"] for x in i["partition"]) @@ -195,7 +201,7 @@ def cron(): WHERE mi.project_id = %(project_id)s AND sessions.project_id = %(project_id)s AND sessions.duration IS NOT NULL AND sessions.start_ts >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT GROUP BY type - ORDER BY issue_count DESC;""", {"project_id": p["project_id"]})) + ORDER BY issue_count DESC;""", params)) issues_breakdown_list = cur.fetchall() if len(issues_breakdown_list) > 4: others = {"type": "Others", diff --git a/api/requirements.txt b/api/requirements.txt index 671aa5da5..f211cec45 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,5 +1,5 @@ -requests==2.24.0 -urllib3==1.25.11 +requests==2.26.0 +urllib3==1.26.6 boto3==1.16.1 pyjwt==1.7.1 psycopg2-binary==2.8.6 diff --git a/backend/Dockerfile b/backend/Dockerfile index 3e60e0e89..8353b8f63 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -41,7 +41,7 @@ ENV TZ=UTC \ AWS_REGION_WEB=eu-central-1 \ AWS_REGION_IOS=eu-west-1 \ AWS_REGION_ASSETS=eu-central-1 \ - CACHE_ASSETS=false \ + CACHE_ASSETS=true \ ASSETS_SIZE_LIMIT=6291456 \ FS_CLEAN_HRS=72 diff --git a/backend/pkg/db/cache/messages_common.go b/backend/pkg/db/cache/messages_common.go index 0b7d9a885..c05422cb2 100644 --- a/backend/pkg/db/cache/messages_common.go +++ b/backend/pkg/db/cache/messages_common.go @@ -28,3 +28,52 @@ func (c *PGCache) InsertIssueEvent(sessionID uint64, crash *IssueEvent) error { } return c.Conn.InsertIssueEvent(sessionID, session.ProjectID, crash) } + + +func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error { + if err := c.Conn.InsertIOSUserID(sessionID, userID); err != nil { + return err + } + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + session.UserID = &userID.Value + return nil +} + +func (c *PGCache) InsertUserAnonymousID(sessionID uint64, userAnonymousID *IOSUserAnonymousID) error { + if err := c.Conn.InsertIOSUserAnonymousID(sessionID, userAnonymousID); err != nil { + return err + } + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + session.UserAnonymousID = &userAnonymousID.Value + return nil +} + +func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + project, err := c.GetProject(session.ProjectID) + if err != nil { + return err + } + + keyNo := project.GetMetadataNo(metadata.Key) + + if keyNo == 0 { + // insert project metadata + } + + if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { + return err + } + + session.SetMetadata(keyNo, metadata.Value) + return nil +} diff --git a/backend/pkg/db/cache/messages_ios.go b/backend/pkg/db/cache/messages_ios.go index 151ffe58e..f630de53d 100644 --- a/backend/pkg/db/cache/messages_ios.go +++ b/backend/pkg/db/cache/messages_ios.go @@ -95,46 +95,3 @@ func (c *PGCache) InsertIOSIssueEvent(sessionID uint64, issueEvent *IOSIssueEven return nil } -func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error { - if err := c.Conn.InsertIOSUserID(sessionID, userID); err != nil { - return err - } - session, err := c.GetSession(sessionID) - if err != nil { - return err - } - session.UserID = &userID.Value - return nil -} - -func (c *PGCache) InsertUserAnonymousID(sessionID uint64, userAnonymousID *IOSUserAnonymousID) error { - if err := c.Conn.InsertIOSUserAnonymousID(sessionID, userAnonymousID); err != nil { - return err - } - session, err := c.GetSession(sessionID) - if err != nil { - return err - } - session.UserAnonymousID = &userAnonymousID.Value - return nil -} - -func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error { - session, err := c.GetSession(sessionID) - if err != nil { - return err - } - project, err := c.GetProject(session.ProjectID) - if err != nil { - return err - } - - keyNo := project.GetMetadataNo(metadata.Key) - if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { - return err - } - - session.SetMetadata(keyNo, metadata.Value) - return nil -} - diff --git a/backend/pkg/db/cache/project.go b/backend/pkg/db/cache/project.go index dacb46633..1411e608b 100644 --- a/backend/pkg/db/cache/project.go +++ b/backend/pkg/db/cache/project.go @@ -11,7 +11,7 @@ func (c *PGCache) GetProjectByKey(projectKey string) (*Project, error) { return c.projectsByKeys[ projectKey ].Project, nil } p, err := c.Conn.GetProjectByKey(projectKey) - if p == nil { + if err != nil { return nil, err } c.projectsByKeys[ projectKey ] = &ProjectMeta{ p, time.Now().Add(c.projectExpirationTimeout) } @@ -27,7 +27,7 @@ func (c *PGCache) GetProject(projectID uint32) (*Project, error) { return c.projects[ projectID ].Project, nil } p, err := c.Conn.GetProject(projectID) - if p == nil { + if err != nil { return nil, err } c.projects[ projectID ] = &ProjectMeta{ p, time.Now().Add(c.projectExpirationTimeout) } diff --git a/backend/pkg/db/postgres/errors.go b/backend/pkg/db/postgres/errors.go index 9012bfe6b..a83c8f03a 100644 --- a/backend/pkg/db/postgres/errors.go +++ b/backend/pkg/db/postgres/errors.go @@ -2,15 +2,17 @@ package postgres import ( "errors" - + + "github.com/jackc/pgx/v4" "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" ) func IsPkeyViolation(err error) bool { var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation { - return true - } - return false -} \ No newline at end of file + return errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation +} + +func IsNoRowsErr(err error) bool { + return err == pgx.ErrNoRows +} diff --git a/backend/pkg/db/postgres/project.go b/backend/pkg/db/postgres/project.go index 461db66fb..2eea30662 100644 --- a/backend/pkg/db/postgres/project.go +++ b/backend/pkg/db/postgres/project.go @@ -1,7 +1,6 @@ package postgres import ( - "github.com/jackc/pgx/v4" . "openreplay/backend/pkg/db/types" ) @@ -14,9 +13,6 @@ func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) { `, projectKey, ).Scan(&p.MaxSessionDuration, &p.SampleRate, &p.ProjectID); err != nil { - if err == pgx.ErrNoRows { - err = nil - } return nil, err } return p, nil @@ -36,9 +32,6 @@ func (conn *Conn) GetProject(projectID uint32) (*Project, error) { ).Scan(&p.ProjectKey,&p.MaxSessionDuration, &p.Metadata1, &p.Metadata2, &p.Metadata3, &p.Metadata4, &p.Metadata5, &p.Metadata6, &p.Metadata7, &p.Metadata8, &p.Metadata9, &p.Metadata10); err != nil { - if err == pgx.ErrNoRows { - err = nil - } return nil, err } return p, nil diff --git a/backend/pkg/env/vars.go b/backend/pkg/env/vars.go index a5334b301..33ae9da3c 100644 --- a/backend/pkg/env/vars.go +++ b/backend/pkg/env/vars.go @@ -36,8 +36,13 @@ func Uint16(key string) uint16 { return uint16(n) } +const MAX_INT = uint64(^uint(0) >> 1) func Int(key string) int { - return int(Uint64(key)) + val := Uint64(key) + if val > MAX_INT { + log.Fatalln(key + " is too big. ") + } + return int(val) } func Bool(key string) bool { diff --git a/backend/pkg/messages/filters.go b/backend/pkg/messages/filters.go index d0a17577b..80525a2cd 100644 --- a/backend/pkg/messages/filters.go +++ b/backend/pkg/messages/filters.go @@ -3,7 +3,7 @@ package messages func IsReplayerType(id uint64) bool { - return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 90 == id || 93 == id || 100 == id || 102 == id || 103 == id || 105 == id + return 0 == id || 2 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 69 == id || 70 == id || 90 == id || 93 == id || 100 == id || 102 == id || 103 == id || 105 == id } func IsIOSType(id uint64) bool { diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index e150256c3..2f1865884 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -86,6 +86,18 @@ p = WriteString(msg.UserCountry, buf, p) return buf[:p] } +type SessionDisconnect struct { + *meta + Timestamp uint64 +} +func (msg *SessionDisconnect) Encode() []byte{ + buf := make([]byte, 11 ) + buf[0] = 2 + p := 1 + p = WriteUint(msg.Timestamp, buf, p) + return buf[:p] +} + type SessionEnd struct { *meta Timestamp uint64 @@ -1166,6 +1178,20 @@ p = WriteString(msg.Selector, buf, p) return buf[:p] } +type CreateIFrameDocument struct { + *meta + FrameID uint64 +ID uint64 +} +func (msg *CreateIFrameDocument) Encode() []byte{ + buf := make([]byte, 21 ) + buf[0] = 70 + p := 1 + p = WriteUint(msg.FrameID, buf, p) +p = WriteUint(msg.ID, buf, p) + return buf[:p] +} + type IOSSessionStart struct { *meta Timestamp uint64 diff --git a/backend/pkg/messages/primitives.go b/backend/pkg/messages/primitives.go index 0c938d2b2..70952eeab 100644 --- a/backend/pkg/messages/primitives.go +++ b/backend/pkg/messages/primitives.go @@ -49,7 +49,7 @@ func ReadUint(reader io.Reader) (uint64, error) { } if b < 0x80 { if i > 9 || i == 9 && b > 1 { - return x, errors.New("overflow") + return x, errors.New("uint overflow") } return x | uint64(b)<>50, 10) } @@ -48,11 +48,11 @@ func isCachable(rawurl string) bool { func GetFullCachableURL(baseURL string, relativeURL string) (string, bool) { if !isRelativeCachable(relativeURL) { - return "", false + return relativeURL, false } fullURL := ResolveURL(baseURL, relativeURL) if !isCachable(fullURL) { - return "", false + return fullURL, false } return fullURL, true } @@ -77,7 +77,7 @@ func GetCachePathForAssets(sessionID uint64, rawurl string) string { func (r *Rewriter) RewriteURL(sessionID uint64, baseURL string, relativeURL string) string { fullURL, cachable := GetFullCachableURL(baseURL, relativeURL) if !cachable { - return relativeURL + return fullURL } u := url.URL{ diff --git a/backend/services/http/handlers.go b/backend/services/http/handlers.go index 975abe31b..81cd6e9c7 100644 --- a/backend/services/http/handlers.go +++ b/backend/services/http/handlers.go @@ -57,8 +57,8 @@ func startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) { } p, err := pgconn.GetProjectByKey(*req.ProjectKey) - if p == nil { - if err == nil { + if err != nil { + if postgres.IsNoRowsErr(err) { responseWithError(w, http.StatusNotFound, errors.New("Project doesn't exist or is not active")) } else { responseWithError(w, http.StatusInternalServerError, err) // TODO: send error here only on staging diff --git a/backend/services/http/handlers_ios.go b/backend/services/http/handlers_ios.go index 2c874a312..32f4a271a 100644 --- a/backend/services/http/handlers_ios.go +++ b/backend/services/http/handlers_ios.go @@ -51,8 +51,8 @@ package main // return // } // p, err := pgconn.GetProject(uint32(projectID)) -// if p == nil { -// if err == nil { +// if err != nil { +// if postgres.IsNoRowsErr(err) { // responseWithError(w, http.StatusNotFound, errors.New("Project doesn't exist or is not active")) // } else { // responseWithError(w, http.StatusInternalServerError, err) // TODO: send error here only on staging diff --git a/backend/services/http/main.go b/backend/services/http/main.go index 29181718f..dc2eb1720 100644 --- a/backend/services/http/main.go +++ b/backend/services/http/main.go @@ -24,7 +24,7 @@ import ( "openreplay/backend/services/http/uaparser" ) - + var rewriter *assets.Rewriter var producer types.Producer var pgconn *cache.PGCache diff --git a/backend/services/sink/main.go b/backend/services/sink/main.go index 52227805f..4a6ac189d 100644 --- a/backend/services/sink/main.go +++ b/backend/services/sink/main.go @@ -15,7 +15,7 @@ import ( "openreplay/backend/pkg/queue/types" ) - + func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) diff --git a/ee/api/.chalice/config.json b/ee/api/.chalice/config.json index 55ace4632..7705f0fbd 100644 --- a/ee/api/.chalice/config.json +++ b/ee/api/.chalice/config.json @@ -35,7 +35,7 @@ "put_S3_TTL": "20", "sourcemaps_reader": "http://utilities-openreplay.app.svc.cluster.local:9000/sourcemaps", "sourcemaps_bucket": "sourcemaps", - "peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers", + "peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers", "js_cache_bucket": "sessions-assets", "async_Token": "", "EMAIL_HOST": "", diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 2a31fc27a..8dc6ce340 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -1,5 +1,5 @@ -requests==2.24.0 -urllib3==1.25.11 +requests==2.26.0 +urllib3==1.26.6 boto3==1.16.1 pyjwt==1.7.1 psycopg2-binary==2.8.6 diff --git a/ee/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql b/ee/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql new file mode 100644 index 000000000..2139861e3 --- /dev/null +++ b/ee/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql @@ -0,0 +1,11 @@ +BEGIN; + +CREATE INDEX sessions_user_id_useridNN_idx ON sessions (user_id) WHERE user_id IS NOT NULL; +CREATE INDEX sessions_uid_projectid_startts_sessionid_uidNN_durGTZ_idx ON sessions (user_id, project_id, start_ts, session_id) WHERE user_id IS NOT NULL AND duration > 0; +CREATE INDEX pages_base_path_base_pathLNGT2_idx ON events.pages (base_path) WHERE length(base_path) > 2; + +CREATE INDEX users_tenant_id_deleted_at_N_idx ON users (tenant_id) WHERE deleted_at ISNULL; +CREATE INDEX issues_issue_id_timestamp_idx ON events_common.issues(issue_id,timestamp); +CREATE INDEX issues_timestamp_idx ON events_common.issues (timestamp); +CREATE INDEX issues_project_id_issue_id_idx ON public.issues (project_id, issue_id); +COMMIT; \ No newline at end of file diff --git a/ee/scripts/helm/roles/openreplay/defaults/main.yaml b/ee/scripts/helm/roles/openreplay/defaults/main.yaml index 97d11429b..eb9071ff3 100644 --- a/ee/scripts/helm/roles/openreplay/defaults/main.yaml +++ b/ee/scripts/helm/roles/openreplay/defaults/main.yaml @@ -1,9 +1,9 @@ --- # defaults file for openreplay app_name: "" +db_name: "" db_list: - "minio" - - "nfs-server-provisioner" - "postgresql" - "redis" - "clickhouse" diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.tsx b/frontend/app/components/Assist/ChatControls/ChatControls.tsx index 6ca747455..f4508e9f2 100644 --- a/frontend/app/components/Assist/ChatControls/ChatControls.tsx +++ b/frontend/app/components/Assist/ChatControls/ChatControls.tsx @@ -2,27 +2,26 @@ import React, { useState } from 'react' import stl from './ChatControls.css' import cn from 'classnames' import { Button, Icon } from 'UI' +import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream'; + interface Props { - stream: MediaStream | null, + stream: LocalStream | null, endCall: () => void } function ChatControls({ stream, endCall } : Props) { const [audioEnabled, setAudioEnabled] = useState(true) - const [videoEnabled, setVideoEnabled] = useState(true) + const [videoEnabled, setVideoEnabled] = useState(false) const toggleAudio = () => { if (!stream) { return; } - const aEn = !audioEnabled - stream.getAudioTracks().forEach(track => track.enabled = aEn); - setAudioEnabled(aEn); + setAudioEnabled(stream.toggleAudio()); } const toggleVideo = () => { if (!stream) { return; } - const vEn = !videoEnabled; - stream.getVideoTracks().forEach(track => track.enabled = vEn); - setVideoEnabled(vEn) + stream.toggleVideo() + .then(setVideoEnabled) } return ( diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx index e1e5ba1a6..ff0767ab9 100644 --- a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -6,10 +6,12 @@ import Counter from 'App/components/shared/SessionItem/Counter' import stl from './chatWindow.css' import ChatControls from '../ChatControls/ChatControls' import Draggable from 'react-draggable'; +import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream'; + export interface Props { incomeStream: MediaStream | null, - localStream: MediaStream | null, + localStream: LocalStream | null, userId: String, endCall: () => void } @@ -30,7 +32,7 @@ const ChatWindow: FC = function ChatWindow({ userId, incomeStream, localS
- +
diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 0d51cca91..493ed3cfe 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -7,9 +7,25 @@ import { connectPlayer } from 'Player/store'; import ChatWindow from '../../ChatWindow'; import { callPeer } from 'Player' import { CallingState, ConnectionStatus } from 'Player/MessageDistributor/managers/AssistManager'; +import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream'; +import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream'; + import { toast } from 'react-toastify'; import stl from './AassistActions.css' +function onClose(stream) { + stream.getTracks().forEach(t=>t.stop()); +} + +function onReject() { + toast.info(`Call was rejected.`); +} + +function onError(e) { + toast.error(e); +} + + interface Props { userId: String, toggleChatWindow: (state) => void, @@ -19,7 +35,7 @@ interface Props { function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus }: Props) { const [ incomeStream, setIncomeStream ] = useState(null); - const [ localStream, setLocalStream ] = useState(null); + const [ localStream, setLocalStream ] = useState(null); const [ endCall, setEndCall ] = useState<()=>void>(()=>{}); useEffect(() => { @@ -32,36 +48,18 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus } }, [peerConnectionStatus]) - function onClose(stream) { - stream.getTracks().forEach(t=>t.stop()); - } - - function onReject() { - toast.info(`Call was rejected.`); - } - - function onError(e) { - toast.error(e); - } - - function onCallConnect(lStream) { - setLocalStream(lStream); - setEndCall(() => callPeer( - lStream, - setIncomeStream, - onClose.bind(null, lStream), - onReject, - onError - )); - } function call() { - navigator.mediaDevices.getUserMedia({video:true, audio:true}) - .then(onCallConnect).catch(error => { // TODO retry only if specific error - navigator.mediaDevices.getUserMedia({audio:true}) - .then(onCallConnect) - .catch(onError) - }); + RequestLocalStream().then(lStream => { + setLocalStream(lStream); + setEndCall(() => callPeer( + lStream, + setIncomeStream, + lStream.stop.bind(lStream), + onReject, + onError + )); + }).catch(onError) } const inCall = calling !== CallingState.False; diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 3aa234b26..bac9ea8e6 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -25,10 +25,9 @@ import { LAST_7_DAYS } from 'Types/app/period'; import { resetFunnel } from 'Duck/funnels'; import { resetFunnelFilters } from 'Duck/funnelFilters' import NoSessionsMessage from '../shared/NoSessionsMessage'; +import TrackerUpdateMessage from '../shared/TrackerUpdateMessage'; import LiveSessionList from './LiveSessionList' -const AUTOREFRESH_INTERVAL = 10 * 60 * 1000; - const weakEqual = (val1, val2) => { if (!!val1 === false && !!val2 === false) return true; if (!val1 !== !val2) return false; @@ -37,7 +36,6 @@ const weakEqual = (val1, val2) => { @withLocationHandlers() @connect(state => ({ - shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0, filter: state.getIn([ 'filters', 'appliedFilter' ]), showLive: state.getIn([ 'user', 'account', 'appearance', 'sessionsLive' ]), variables: state.getIn([ 'customFields', 'list' ]), @@ -93,12 +91,6 @@ export default class BugFinder extends React.PureComponent { this.props.resetFunnel(); this.props.resetFunnelFilters(); - this.autorefreshIntervalId = setInterval(() => { - if (this.props.shouldAutorefresh) { - props.applyFilter(); - } - }, AUTOREFRESH_INTERVAL); - props.fetchFunnelsList(LAST_7_DAYS) } @@ -129,10 +121,6 @@ export default class BugFinder extends React.PureComponent { }.bind(this)); } - componentWillUnmount() { - clearInterval(this.autorefreshIntervalId); - } - setActiveTab = tab => { this.props.setActiveTab(tab); } @@ -151,6 +139,7 @@ export default class BugFinder extends React.PureComponent { />
+
, fetchList: (params) => void, + applyFilter: () => void, filters: List } function LiveSessionList(props: Props) { const { loading, list, filters } = props; + var timeoutId; useEffect(() => { props.fetchList(filters.toJS()); + timeout(); + return () => { + clearTimeout(timeoutId) + } }, []) + const timeout = () => { + timeoutId = setTimeout(() => { + props.fetchList(filters.toJS()); + timeout(); + }, AUTOREFRESH_INTERVAL); + } + return (
+ See how to {'enable Assist'} if you haven't yet done so. + + } image={} show={ !loading && list && list.size === 0} > diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index 41375a6af..0aa4b9afb 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -4,12 +4,14 @@ import { applyFilter, addAttribute, addEvent } from 'Duck/filters'; import SessionItem from 'Shared/SessionItem'; import SessionListHeader from './SessionListHeader'; import { KEYS } from 'Types/filter/customFilter'; -import styles from './sessionList.css'; const ALL = 'all'; const PER_PAGE = 10; +const AUTOREFRESH_INTERVAL = 3 * 60 * 1000; +var timeoutId; @connect(state => ({ + shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0, savedFilters: state.getIn([ 'filters', 'list' ]), loading: state.getIn([ 'sessions', 'loading' ]), activeTab: state.getIn([ 'sessions', 'activeTab' ]), @@ -27,6 +29,7 @@ export default class SessionList extends React.PureComponent { } constructor(props) { super(props); + this.timeout(); } componentDidUpdate(prevProps) { @@ -47,6 +50,15 @@ export default class SessionList extends React.PureComponent { this.props.applyFilter() } + timeout = () => { + timeoutId = setTimeout(function () { + if (this.props.shouldAutorefresh) { + this.props.applyFilter(); + } + this.timeout(); + }.bind(this), AUTOREFRESH_INTERVAL); + } + getNoContentMessage = activeTab => { let str = "No recordings found"; if (activeTab.type !== 'all') { @@ -57,6 +69,10 @@ export default class SessionList extends React.PureComponent { return str + '!'; } + componentWillUnmount() { + clearTimeout(timeoutId) + } + renderActiveTabContent(list) { const { loading, diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index 129862b29..48cb9c0f8 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -7,6 +7,7 @@ import { fetchWatchdogStatus } from 'Duck/watchdogs'; import { setActiveFlow, clearEvents } from 'Duck/filters'; import { setActiveTab } from 'Duck/sessions'; import { issues_types } from 'Types/session/issue' +import NewBadge from 'Shared/NewBadge'; function SessionsMenu(props) { const { @@ -75,11 +76,15 @@ function SessionsMenu(props) {
+
Assist
+
{ }
+
} iconName="person" active={activeTab.type === 'live'} onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })} /> +
diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index 01d3e7ad2..a0aef3cad 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,60 +1,53 @@ import Highlight from 'react-highlight' import ToggleContent from 'Shared/ToggleContent' import DocLink from 'Shared/DocLink/DocLink'; +import AssistScript from './AssistScript' +import AssistNpm from './AssistNpm' +import { Tabs } from 'UI'; +import { useState } from 'react'; + +const NPM = 'NPM' +const SCRIPT = 'SCRIPT' +const TABS = [ + { key: SCRIPT, text: SCRIPT }, + { key: NPM, text: NPM }, +] const AssistDoc = (props) => { + const { projectKey } = props; + const [activeTab, setActiveTab] = useState(SCRIPT) + + + const renderActiveTab = () => { + switch (activeTab) { + case SCRIPT: + return + case NPM: + return + } + return null; + } + + return (
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.
- +
Installation
{`npm i @openreplay/tracker-assist`} - -
Usage
-

Initialize the tracker then load the @openreplay/tracker-assist plugin.

-
+
Usage
- - {`import Tracker from '@openreplay/tracker'; -import trackerAssist from '@openreplay/tracker-assist'; -const tracker = new Tracker({ - projectKey: PROJECT_KEY, -}); -tracker.start(); -tracker.use(trackerAssist(options)); // check the list of available options below`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; -import trackerFetch from '@openreplay/tracker-assist/cjs'; -const tracker = new OpenReplay({ - projectKey: PROJECT_KEY -}); -const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below -//... -function MyApp() { - useEffect(() => { // use componentDidMount in case of React Class Component - tracker.start(); - }, []) -//... -}`} - - } + setActiveTab(tab) } /> -
Options
- - {`trackerAssist({ - confirmText: string; -})`} - +
+ { renderActiveTab() } +
diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistNpm.tsx b/frontend/app/components/Client/Integrations/AssistDoc/AssistNpm.tsx new file mode 100644 index 000000000..28c12bd30 --- /dev/null +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistNpm.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Highlight from 'react-highlight' +import ToggleContent from 'Shared/ToggleContent' + +function AssistNpm(props) { + return ( +
+

Initialize the tracker then load the @openreplay/tracker-assist plugin.

+ +
Usage
+ + {`import Tracker from '@openreplay/tracker'; +import trackerAssist from '@openreplay/tracker-assist'; +const tracker = new Tracker({ + projectKey: '${props.projectKey}', +}); +tracker.start(); +tracker.use(trackerAssist(options)); // check the list of available options below`} + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; +import trackerFetch from '@openreplay/tracker-assist/cjs'; +const tracker = new OpenReplay({ + projectKey: '${props.projectKey}' +}); +const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below +//... +function MyApp() { + useEffect(() => { // use componentDidMount in case of React Class Component + tracker.start(); + }, []) +//... +}`} + + } + /> + +
Options
+ + {`trackerAssist({ + confirmText: string; +})`} + +
+ ); +} + +export default AssistNpm; \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistScript.tsx b/frontend/app/components/Client/Integrations/AssistDoc/AssistScript.tsx new file mode 100644 index 000000000..932926a1a --- /dev/null +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistScript.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import Highlight from 'react-highlight' + +function AssistScript(props) { + return ( +
+

If your OpenReplay tracker is set up using the JS snippet, then simply replace the .../openreplay.js occurrence with .../openreplay-assist.js. Below is an example of how the script should like after the change:

+
+ + + {` +`} + +
+ ); +} + +export default AssistScript; \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js index d8238c21f..8ba8e590d 100644 --- a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js +++ b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js @@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent' import DocLink from 'Shared/DocLink/DocLink'; const FetchDoc = (props) => { + const { projectKey } = props; return (
This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
@@ -18,14 +19,14 @@ const FetchDoc = (props) => {
Usage
{`import tracker from '@openreplay/tracker'; import trackerFetch from '@openreplay/tracker-fetch'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.start(); //... @@ -40,7 +41,7 @@ fetch('https://api.openreplay.com/').then(response => console.log(response.json( import trackerFetch from '@openreplay/tracker-fetch/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index b1b648f99..7a964de3a 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -3,6 +3,7 @@ import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; const GraphQLDoc = (props) => { + const { projectKey } = props; return (

This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.

@@ -19,14 +20,14 @@ const GraphQLDoc = (props) => {
{`import OpenReplay from '@openreplay/tracker'; import trackerGraphQL from '@openreplay/tracker-graphql'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY, + projectKey: '${projectKey}' }); tracker.start(); //... @@ -39,7 +40,7 @@ export const recordGraphQL = tracker.use(trackerGraphQL());`} import trackerGraphQL from '@openreplay/tracker-graphql/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/Integrations/Integrations.js b/frontend/app/components/Client/Integrations/Integrations.js index 05036788e..f34050055 100644 --- a/frontend/app/components/Client/Integrations/Integrations.js +++ b/frontend/app/components/Client/Integrations/Integrations.js @@ -75,7 +75,7 @@ const TITLE = { [ ASSIST ] : 'Assist', } -const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER] +const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER, ASSIST] const integrations = [ 'sentry', 'datadog', 'stackdriver', 'rollbar', 'newrelic', 'bugsnag', 'cloudwatch', 'elasticsearch', 'sumologic', 'issues' ]; @@ -87,12 +87,14 @@ const integrations = [ 'sentry', 'datadog', 'stackdriver', 'rollbar', 'newrelic' state.getIn([ name, 'list' ]).size > 0; props.loading = props.loading || state.getIn([ name, 'fetchRequest', 'loading']); }) + const site = state.getIn([ 'site', 'instance' ]); return { ...props, issues: state.getIn([ 'issues', 'list']).first() || {}, slackChannelListExists: state.getIn([ 'slack', 'list' ]).size > 0, tenantId: state.getIn([ 'user', 'client', 'tenantId' ]), - jwt: state.get('jwt') + jwt: state.get('jwt'), + projectKey: site ? site.projectKey : '' }; }, { fetchList, @@ -142,7 +144,9 @@ export default class Integrations extends React.PureComponent { } } - renderModalContent() { + renderModalContent() { + const { projectKey } = this.props; + switch (this.state.modalContent) { case SENTRY: return ; @@ -172,21 +176,21 @@ export default class Integrations extends React.PureComponent { case JIRA: return ; case REDUX: - return + return case VUE: - return + return case GRAPHQL: - return + return case NGRX: - return + return case FETCH: - return + return case MOBX: - return + return case PROFILER: - return + return case ASSIST: - return + return default: return null; } diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 7c4b3233b..79166da56 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent' import DocLink from 'Shared/DocLink/DocLink'; const MobxDoc = (props) => { + const { projectKey } = props; return (
This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
@@ -18,14 +19,14 @@ const MobxDoc = (props) => {
Usage
{`import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.use(trackerMobX()); // check list of available options below tracker.start();`} @@ -37,7 +38,7 @@ tracker.start();`} import trackerMobX from '@openreplay/tracker-mobx/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.use(trackerMobX()); // check list of available options below //... diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 092e93a7d..9aa441c7e 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent' import DocLink from 'Shared/DocLink/DocLink'; const NgRxDoc = (props) => { + const { projectKey } = props; return (
This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
@@ -18,7 +19,7 @@ const NgRxDoc = (props) => {
Usage
{`import { StoreModule } from '@ngrx/store'; @@ -27,7 +28,7 @@ import OpenReplay from '@openreplay/tracker'; import trackerNgRx from '@openreplay/tracker-ngrx'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.start(); //... @@ -47,7 +48,7 @@ import OpenReplay from '@openreplay/tracker/cjs'; import trackerNgRx from '@openreplay/tracker-ngrx/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index e61f968b9..f00b98815 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent' import DocLink from 'Shared/DocLink/DocLink'; const ProfilerDoc = (props) => { + const { projectKey } = props; return (
The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.
@@ -18,14 +19,14 @@ const ProfilerDoc = (props) => {
Usage
{`import OpenReplay from '@openreplay/tracker'; import trackerProfiler from '@openreplay/tracker-profiler'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.start(); //... @@ -42,7 +43,7 @@ const fn = profiler('call_name')(() => { import trackerProfiler from '@openreplay/tracker-profiler/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index acc083a97..c4fa51240 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -3,6 +3,7 @@ import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const ReduxDoc = (props) => { + const { projectKey } = props; return (
This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
@@ -17,7 +18,7 @@ const ReduxDoc = (props) => {

Initialize the tracker then put the generated middleware into your Redux chain.

{`import { applyMiddleware, createStore } from 'redux'; @@ -25,7 +26,7 @@ import OpenReplay from '@openreplay/tracker'; import trackerRedux from '@openreplay/tracker-redux'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.start(); //... @@ -42,7 +43,7 @@ import OpenReplay from '@openreplay/tracker/cjs'; import trackerRedux from '@openreplay/tracker-redux/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index 14a7e435a..b6682411c 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -3,6 +3,7 @@ import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const VueDoc = (props) => { + const { projectKey } = props; return (
This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
@@ -18,7 +19,7 @@ const VueDoc = (props) => { {`import Vuex from 'vuex' @@ -26,7 +27,7 @@ import OpenReplay from '@openreplay/tracker'; import trackerVuex from '@openreplay/tracker-vuex'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); tracker.start(); //... @@ -43,7 +44,7 @@ import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; //... const tracker = new OpenReplay({ - projectKey: PROJECT_KEY + projectKey: '${projectKey}' }); //... function SomeFunctionalComponent() { diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index 6d291ed1d..0d49d5319 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -27,7 +27,7 @@ const LIMIT_WARNING = 'You have reached users limit.'; fetchList, generateInviteLink }) -@withPageTitle('Manage Users - OpenReplay Preferences') +@withPageTitle('Users - OpenReplay Preferences') class ManageUsers extends React.PureComponent { state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false } @@ -50,7 +50,7 @@ class ManageUsers extends React.PureComponent { deleteHandler = async (user) => { if (await confirm({ - header: 'Manage Users', + header: 'Users', confirmation: `Are you sure you want to remove this user?` })) { this.props.deleteMember(user.id).then(() => { diff --git a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js index f2a59dd15..4a1ff3f1d 100644 --- a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js +++ b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js @@ -72,7 +72,7 @@ function PreferencesMenu({ activeTab, appearance, history }) {
setTab(CLIENT_TABS.MANAGE_USERS) } /> diff --git a/frontend/app/components/Client/Sites/NewSiteForm.js b/frontend/app/components/Client/Sites/NewSiteForm.js index a9306048c..011ed8b01 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.js +++ b/frontend/app/components/Client/Sites/NewSiteForm.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { Input, Button, Label } from 'UI'; import { save, edit, update , fetchList } from 'Duck/site'; import { pushNewSite, setSiteId } from 'Duck/user'; +import { withRouter } from 'react-router-dom'; import styles from './siteForm.css'; @connect(state => ({ @@ -17,6 +18,7 @@ import styles from './siteForm.css'; fetchList, setSiteId }) +@withRouter export default class NewSiteForm extends React.PureComponent { state = { existsError: false, @@ -24,7 +26,7 @@ export default class NewSiteForm extends React.PureComponent { onSubmit = e => { e.preventDefault(); - const { site, siteList } = this.props; + const { site, siteList, location: { pathname } } = this.props; if (!site.exists() && siteList.some(({ name }) => name === site.name)) { return this.setState({ existsError: true }); } @@ -39,20 +41,21 @@ export default class NewSiteForm extends React.PureComponent { const site = sites.last(); this.props.pushNewSite(site) - this.props.setSiteId(site.id) + if (!pathname.includes('/client')) { + this.props.setSiteId(site.id) + } this.props.onClose(null, site) }); } } edit = ({ target: { name, value } }) => { - if (value.includes(' ')) return; // TODO: site validation this.setState({ existsError: false }); this.props.edit({ [ name ]: value }); } render() { - const { site, loading, onClose } = this.props; + const { site, loading } = this.props; return (
diff --git a/frontend/app/components/Header/OnboardingExplore/featureItem.css b/frontend/app/components/Header/OnboardingExplore/featureItem.css index b0fe2dbb9..70a0f3d05 100644 --- a/frontend/app/components/Header/OnboardingExplore/featureItem.css +++ b/frontend/app/components/Header/OnboardingExplore/featureItem.css @@ -34,6 +34,7 @@ .activeLink { cursor: pointer; pointer-events: default; + text-decoration: underline; & label { color: #000000 !important; text-decoration: underline; diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js b/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js index 6ec2db234..fd05da5b6 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js @@ -22,7 +22,6 @@ class TrackingCodeModal extends React.PureComponent { } renderActiveTab = () => { - console.log('rendering...') switch (this.state.activeTab) { case PROJECT: return diff --git a/frontend/app/components/Session/RightBlock.tsx b/frontend/app/components/Session/RightBlock.tsx index be88b9e8e..3eb802f68 100644 --- a/frontend/app/components/Session/RightBlock.tsx +++ b/frontend/app/components/Session/RightBlock.tsx @@ -4,9 +4,10 @@ import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel' import { Controls as PlayerControls } from 'Player'; import { Tabs } from 'UI'; import { connectPlayer } from 'Player'; +import NewBadge from 'Shared/NewBadge'; const EVENTS = 'Events'; -const HEATMAPS = 'Heatmaps'; +const HEATMAPS = 'Click Map'; const TABS = [ EVENTS, HEATMAPS ].map(tab => ({ text: tab, key: tab })); @@ -29,12 +30,15 @@ export default function RightBlock() { } return (
- setActiveTab(tab) } - border={ true } - /> +
+ setActiveTab(tab) } + border={ true } + /> +
{ }
+
{ renderActiveTab(activeTab) } diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.js b/frontend/app/components/Session_/EventsBlock/EventsBlock.js index 53a83aaa9..0190ed23c 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.js +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.js @@ -42,7 +42,9 @@ export default class EventsBlock extends React.PureComponent { this.props.setEventFilter({ query: value, filter }) setTimeout(() => { - this.scroller.current.scrollToRow(0); + if (!this.scroller.current) return; + + this.scroller.current.scrollToRow(0); }, 100) } @@ -54,7 +56,9 @@ export default class EventsBlock extends React.PureComponent { this.scroller.current.forceUpdateGrid(); setTimeout(() => { - this.scroller.current.scrollToRow(0); + if (!this.scroller.current) return; + + this.scroller.current.scrollToRow(0); }, 100) } @@ -176,6 +180,7 @@ export default class EventsBlock extends React.PureComponent { userNumericHash, userDisplayName, userId, + revId, userAnonymousId }, filteredEvents @@ -191,6 +196,7 @@ export default class EventsBlock extends React.PureComponent { userNumericHash={userNumericHash} userDisplayName={userDisplayName} userId={userId} + revId={revId} userAnonymousId={userAnonymousId} /> diff --git a/frontend/app/components/Session_/EventsBlock/UserCard/UserCard.js b/frontend/app/components/Session_/EventsBlock/UserCard/UserCard.js index ecd072498..c2b138044 100644 --- a/frontend/app/components/Session_/EventsBlock/UserCard/UserCard.js +++ b/frontend/app/components/Session_/EventsBlock/UserCard/UserCard.js @@ -6,7 +6,7 @@ import Metadata from '../Metadata' import { withRequest } from 'HOCs' import SessionList from '../Metadata/SessionList' -function UserCard({ className, userNumericHash, userDisplayName, similarSessions, userId, userAnonymousId, request, loading }) { +function UserCard({ className, userNumericHash, userDisplayName, similarSessions, userId, userAnonymousId, request, loading, revId }) { const [showUserSessions, setShowUserSessions] = useState(false) const hasUserDetails = !!userId || !!userAnonymousId; @@ -29,6 +29,11 @@ function UserCard({ className, userNumericHash, userDisplayName, similarSessions
+ {revId && ( +
+ Rev ID: {revId} +
+ )}
diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js index 9cd646fe1..6a2c75426 100644 --- a/frontend/app/components/Session_/Exceptions/Exceptions.js +++ b/frontend/app/components/Session_/Exceptions/Exceptions.js @@ -14,7 +14,7 @@ import BottomBlock from '../BottomBlock'; @connect(state => ({ session: state.getIn([ 'sessions', 'current' ]), errorStack: state.getIn([ 'sessions', 'errorStack' ]), - sourceMapUploaded: state.getIn([ 'sessions', 'sourceMapUploaded' ]), + sourcemapUploaded: state.getIn([ 'sessions', 'sourcemapUploaded' ]), loading: state.getIn([ 'sessions', 'fetchErrorStackList', 'loading' ]) }), { fetchErrorStackList }) export default class Exceptions extends React.PureComponent { @@ -33,7 +33,7 @@ export default class Exceptions extends React.PureComponent { closeModal = () => this.setState({ currentError: null}) render() { - const { exceptions, loading, errorStack, sourceMapUploaded } = this.props; + const { exceptions, loading, errorStack, sourcemapUploaded } = this.props; const { filter, currentError } = this.state; const filterRE = getRE(filter, 'i'); @@ -63,7 +63,7 @@ export default class Exceptions extends React.PureComponent { show={ !loading && errorStack.size === 0 } title="Nothing found!" > - +
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 848e17153..266cf1cd1 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -399,7 +399,7 @@ export default class Controls extends React.Component { icon="tachometer-slow" /> } - { !live && showLongtasks && + {/* { !live && showLongtasks && toggleBottomBlock(LONGTASKS) } @@ -407,7 +407,7 @@ export default class Controls extends React.Component { label="Long Tasks" icon="business-time" /> - } + } */}
{ !live && diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js index 7df2822ee..c12c3dcc8 100644 --- a/frontend/app/components/Signup/SignupForm/SignupForm.js +++ b/frontend/app/components/Signup/SignupForm/SignupForm.js @@ -26,11 +26,24 @@ export default class SignupForm extends React.Component { email: '', projectName: '', organizationName: '', + reload: false, }; + static getDerivedStateFromProps(props, state) { + if (props.errors && props.errors.size > 0 && state.reload) { + recaptchaRef.current.reset(); + return { + reload: false + } + } + return null; + } + + handleSubmit = (token) => { const { tenantId, fullname, password, email, projectName, organizationName, auth } = this.state; this.props.signup({ tenantId, fullname, password, email, projectName, organizationName, auth, 'g-recaptcha-response': token }) + this.setState({ reload: true }) } write = ({ target: { value, name } }) => this.setState({ [ name ]: value }) diff --git a/frontend/app/components/shared/TrackerUpdateMessage/TrackerUpdateMessage.js b/frontend/app/components/shared/TrackerUpdateMessage/TrackerUpdateMessage.js new file mode 100644 index 000000000..790af890d --- /dev/null +++ b/frontend/app/components/shared/TrackerUpdateMessage/TrackerUpdateMessage.js @@ -0,0 +1,40 @@ +import React from 'react' +import { Icon } from 'UI' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom'; +import { onboarding as onboardingRoute } from 'App/routes' +import { withSiteId } from 'App/routes'; + +const TrackerUpdateMessage= (props) => { + // const { site } = props; + const { site, sites, match: { params: { siteId } } } = props; + const activeSite = sites.find(s => s.id == siteId); + const hasSessions = !!activeSite && !activeSite.recorded; + const needUpdate = !hasSessions && site.trackerVersion !== window.ENV.TRACKER_VERSION; + return needUpdate ? ( + <> + {( +
+
+
+
+ +
+
+ There might be a mismatch between the tracker and the backend versions. Please make sure to props.history.push(withSiteId(onboardingRoute('installing'), siteId))}>update the tracker to latest version ({window.ENV.TRACKER_VERSION}). +
+
+
+
+ )} + + ) : '' +} + +export default connect(state => ({ + site: state.getIn([ 'site', 'instance' ]), + sites: state.getIn([ 'site', 'list' ]) +}))(withRouter(TrackerUpdateMessage)) \ No newline at end of file diff --git a/frontend/app/components/shared/TrackerUpdateMessage/index.js b/frontend/app/components/shared/TrackerUpdateMessage/index.js new file mode 100644 index 000000000..b9f95895d --- /dev/null +++ b/frontend/app/components/shared/TrackerUpdateMessage/index.js @@ -0,0 +1 @@ +export { default } from './TrackerUpdateMessage' \ No newline at end of file diff --git a/frontend/app/components/ui/SavedSearchList/SavedSearchList.js b/frontend/app/components/ui/SavedSearchList/SavedSearchList.js index 3bbd8d05b..fc82f1ef0 100644 --- a/frontend/app/components/ui/SavedSearchList/SavedSearchList.js +++ b/frontend/app/components/ui/SavedSearchList/SavedSearchList.js @@ -19,7 +19,6 @@ import { funnel as funnelRoute, withSiteId } from 'App/routes'; import Event, { TYPES } from 'Types/filter/event'; import FunnelMenuItem from 'Components/Funnels/FunnelMenuItem'; import FunnelSaveModal from 'Components/Funnels/FunnelSaveModal'; -import NewBadge from 'Shared/NewBadge'; import { blink as setBlink } from 'Duck/funnels'; const DEFAULT_VISIBLE = 3; @@ -98,7 +97,6 @@ class SavedSearchList extends React.Component { onClick={ this.createHandler } /> )} -
{ }
{ funnels.size === 0 && diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts index 8f8b16e6c..9e508de25 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -5,11 +5,13 @@ import type { TimedMessage } from '../Timed'; import type { Message } from '../messages' import { ID_TP_MAP } from '../messages'; import store from 'App/store'; +import type { LocalStream } from './LocalStream'; import { update, getState } from '../../store'; export enum CallingState { + Reconnecting, Requesting, True, False, @@ -38,7 +40,7 @@ export function getStatusText(status: ConnectionStatus): string { case ConnectionStatus.Error: return "Something went wrong. Try to reload the page."; case ConnectionStatus.WaitingMessages: - return "Connected. Waiting for the data..." + return "Connected. Waiting for the data... (The tab might be inactive)" } } @@ -161,7 +163,6 @@ export default class AssistManager { if (['peer-unavailable', 'network', 'webrtc'].includes(e.type)) { if (this.peer && this.connectionAttempts++ < MAX_RECONNECTION_COUNT) { this.setStatus(ConnectionStatus.Connecting); - console.log("peerunavailable") this.connectToPeer(); } else { this.setStatus(ConnectionStatus.Disconnected); @@ -175,7 +176,6 @@ export default class AssistManager { peer.on("open", () => { if (this.peeropened) { return; } this.peeropened = true; - console.log('peeropen') this.connectToPeer(); }); }); @@ -186,11 +186,16 @@ export default class AssistManager { if (!this.peer) { return; } this.setStatus(ConnectionStatus.Connecting); const id = this.peerID; - console.log("trying to connect to", id) const conn = this.peer.connect(id, { serialization: 'json', reliable: true}); conn.on('open', () => { window.addEventListener("beforeunload", ()=>conn.open &&conn.send("unload")); - console.log("peer connected") + + //console.log("peer connected") + + + if (getState().calling === CallingState.Reconnecting) { + this._call() + } let i = 0; let firstMessage = true; @@ -199,7 +204,7 @@ export default class AssistManager { conn.on('data', (data) => { if (!Array.isArray(data)) { return this.handleCommand(data); } - this.mesagesRecieved = true; + this.disconnectTimeout && clearTimeout(this.disconnectTimeout); if (firstMessage) { firstMessage = false; this.setStatus(ConnectionStatus.Connected) @@ -250,9 +255,8 @@ export default class AssistManager { const onDataClose = () => { - this.initiateCallEnd(); - this.setStatus(ConnectionStatus.Connecting); - console.log('closed peer conn. Reconnecting...') + this.onCallDisconnect() + //console.log('closed peer conn. Reconnecting...') this.connectToPeer(); } @@ -263,7 +267,6 @@ export default class AssistManager { // }, 3000); conn.on('close', onDataClose);// Does it work ? conn.on("error", (e) => { - console.log("PeerJS connection error", e); this.setStatus(ConnectionStatus.Error); }) } @@ -282,49 +285,49 @@ export default class AssistManager { } - private onCallEnd: null | (()=>void) = null; - private onReject: null | (()=>void) = null; private forceCallEnd() { this.callConnection?.close(); } private notifyCallEnd() { const dataConn = this.dataConnection; if (dataConn) { - console.log("notifyCallEnd send") dataConn.send("call_end"); } } private initiateCallEnd = () => { - console.log('initiateCallEnd') this.forceCallEnd(); this.notifyCallEnd(); - this.onCallEnd?.(); + this.localCallData && this.localCallData.onCallEnd(); } private onTrackerCallEnd = () => { + console.log('onTrackerCallEnd') this.forceCallEnd(); if (getState().calling === CallingState.Requesting) { - this.onReject?.(); + this.localCallData && this.localCallData.onReject(); + } + this.localCallData && this.localCallData.onCallEnd(); + } + + private onCallDisconnect = () => { + if (getState().calling === CallingState.True) { + update({ calling: CallingState.Reconnecting }); } - this.onCallEnd?.(); } - - private mesagesRecieved: boolean = false; + private disconnectTimeout: ReturnType | undefined; private handleCommand(command: string) { + console.log("Data command", command) switch (command) { case "unload": - this.onTrackerCallEnd(); - this.mesagesRecieved = false; - setTimeout(() => { - if (this.mesagesRecieved) { - return; - } - // @ts-ignore - this.dataConnection?.close(); + //this.onTrackerCallEnd(); + this.onCallDisconnect() + this.dataConnection?.close(); + this.disconnectTimeout = setTimeout(() => { + this.onTrackerCallEnd(); this.setStatus(ConnectionStatus.Disconnected); - }, 8000); // TODO: more convenient way + }, 15000); // TODO: more convenient way //this.dataConnection?.close(); return; case "call_end": @@ -345,63 +348,80 @@ export default class AssistManager { conn.send({ x: Math.round(data.x), y: Math.round(data.y) }); } - call(localStream: MediaStream, onStream: (s: MediaStream)=>void, onCallEnd: () => void, onReject: () => void, onError?: ()=> void): null | Function { - if (!this.peer || getState().calling !== CallingState.False) { return null; } + + private localCallData: { + localStream: LocalStream, + onStream: (s: MediaStream)=>void, + onCallEnd: () => void, + onReject: () => void, + onError?: ()=> void + } | null = null + + call(localStream: LocalStream, onStream: (s: MediaStream)=>void, onCallEnd: () => void, onReject: () => void, onError?: ()=> void): null | Function { + this.localCallData = { + localStream, + onStream, + onCallEnd: () => { + onCallEnd(); + this.md.overlay.removeEventListener("mousemove", this.onMouseMove); + update({ calling: CallingState.False }); + this.localCallData = null; + }, + onReject, + onError, + } + this._call() + return this.initiateCallEnd; + } + + private _call() { + if (!this.peer || !this.localCallData || ![CallingState.False, CallingState.Reconnecting].includes(getState().calling)) { return null; } update({ calling: CallingState.Requesting }); - console.log('calling...') - - const call = this.peer.call(this.peerID, localStream); - call.on('stream', stream => { - //call.peerConnection.ontrack = (t)=> console.log('ontrack', t) + //console.log('calling...', this.localCallData.localStream) + + const call = this.peer.call(this.peerID, this.localCallData.localStream.stream); + this.localCallData.localStream.onVideoTrack(vTrack => { + const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video") + if (!sender) { + //logger.warn("No video sender found") + return + } + //logger.log("sender found:", sender) + sender.replaceTrack(vTrack) + }) + + call.on('stream', stream => { update({ calling: CallingState.True }); - onStream(stream); + this.localCallData && this.localCallData.onStream(stream); this.send({ name: store.getState().getIn([ 'user', 'account', 'name']), }); - // @ts-ignore ?? this.md.overlay.addEventListener("mousemove", this.onMouseMove) + }); + //call.peerConnection.addEventListener("track", e => console.log('newtrack',e.track)) - this.onCallEnd = () => { - onCallEnd(); - // @ts-ignore ?? - this.md.overlay.removeEventListener("mousemove", this.onMouseMove); - update({ calling: CallingState.False }); - this.onCallEnd = null; - } - - call.on("close", this.onCallEnd); + call.on("close", this.localCallData.onCallEnd); call.on("error", (e) => { console.error("PeerJS error (on call):", e) - this.initiateCallEnd?.(); - onError?.(); + this.initiateCallEnd(); + this.localCallData && this.localCallData.onError && this.localCallData.onError(); }); - // const intervalID = setInterval(() => { - // if (!call.open && getState().calling === CallingState.True) { - // this.onCallEnd?.(); - // clearInterval(intervalID); - // } - // }, 5000); - window.addEventListener("beforeunload", this.initiateCallEnd) - - return this.initiateCallEnd; } clear() { - console.log('clearing', this.peerID) this.initiateCallEnd(); this.dataCheckIntervalID && clearInterval(this.dataCheckIntervalID); if (this.peer) { - this.peer.connections[this.peerID]?.forEach(c => c.open && c.close()); - console.log("destroying peer...") - this.peer.disconnect(); - this.peer.destroy(); + //console.log("destroying peer...") + const peer = this.peer; // otherwise it calls reconnection on data chan close this.peer = null; + peer.destroy(); } } } diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts index 1d723a923..e42041f91 100644 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.ts +++ b/frontend/app/player/MessageDistributor/managers/DOMManager.ts @@ -3,7 +3,7 @@ import type { Message, SetNodeScroll, CreateElementNode } from '../messages'; import type { TimedMessage } from '../Timed'; import logger from 'App/logger'; -import StylesManager from './StylesManager'; +import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; import ListWalker from './ListWalker'; import type { Timed }from '../Timed'; @@ -147,6 +147,7 @@ export default class DOMManager extends ListWalker { this.insertNode(msg); break; case "create_element_node": + // console.log('elementnode', msg) if (msg.svg) { this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag); } else { @@ -207,9 +208,14 @@ export default class DOMManager extends ListWalker { break; case "set_node_data": case "set_css_data": - if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; } + node = this.nl[ msg.id ] + if (!node) { logger.error("Node not found", msg); break; } // @ts-ignore - this.nl[ msg.id ].data = msg.data; + node.data = msg.data; + if (node instanceof HTMLStyleElement) { + const doc = this.screen.document + doc && rewriteNodeStyleSheet(doc, node) + } break; case "css_insert_rule": node = this.nl[ msg.id ]; @@ -239,6 +245,22 @@ export default class DOMManager extends ListWalker { } catch (e) { logger.warn(e, msg) } + break; + case "create_i_frame_document": + // console.log('ifr', msg) + node = this.nl[ msg.frameID ]; + if (!(node instanceof HTMLIFrameElement)) { + logger.warn("create_i_frame_document message. Node is not iframe") + return; + } + // await new Promise(resolve => { node.onload = resolve }) + + const doc = node.contentDocument; + if (!doc) { + logger.warn("No iframe doc", msg, node, node.contentDocument); + return; + } + this.nl[ msg.id ] = doc.documentElement break; //not sure what to do with this one //case "disconnected": diff --git a/frontend/app/player/MessageDistributor/managers/LocalStream.ts b/frontend/app/player/MessageDistributor/managers/LocalStream.ts new file mode 100644 index 000000000..63f01ad58 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/LocalStream.ts @@ -0,0 +1,85 @@ +declare global { + interface HTMLCanvasElement { + captureStream(frameRate?: number): MediaStream; + } +} + +function dummyTrack(): MediaStreamTrack { + const canvas = document.createElement("canvas")//, { width: 0, height: 0}) + canvas.width=canvas.height=2 // Doesn't work when 1 (?!) + const ctx = canvas.getContext('2d'); + ctx?.fillRect(0, 0, canvas.width, canvas.height); + requestAnimationFrame(function draw(){ + ctx?.fillRect(0,0, canvas.width, canvas.height) + requestAnimationFrame(draw); + }); + // Also works. Probably it should be done once connected. + //setTimeout(() => { ctx?.fillRect(0,0, canvas.width, canvas.height) }, 4000) + return canvas.captureStream(60).getTracks()[0]; +} + +export default function RequestLocalStream(): Promise { + return navigator.mediaDevices.getUserMedia({ audio:true }) + .then(aStream => { + const aTrack = aStream.getAudioTracks()[0] + if (!aTrack) { throw new Error("No audio tracks provided") } + return new _LocalStream(aTrack) + }) +} + +class _LocalStream { + private mediaRequested: boolean = false + readonly stream: MediaStream + private readonly vdTrack: MediaStreamTrack + constructor(aTrack: MediaStreamTrack) { + this.vdTrack = dummyTrack() + this.stream = new MediaStream([ aTrack, this.vdTrack ]) + } + + toggleVideo(): Promise { + if (!this.mediaRequested) { + return navigator.mediaDevices.getUserMedia({video:true}) + .then(vStream => { + const vTrack = vStream.getVideoTracks()[0] + if (!vTrack) { + throw new Error("No video track provided") + } + this.stream.addTrack(vTrack) + this.stream.removeTrack(this.vdTrack) + this.mediaRequested = true + if (this.onVideoTrackCb) { + this.onVideoTrackCb(vTrack) + } + return true + }) + .catch(e => { + // TODO: log + return false + }) + } + let enabled = true + this.stream.getVideoTracks().forEach(track => { + track.enabled = enabled = enabled && !track.enabled + }) + return Promise.resolve(enabled) + } + + toggleAudio(): boolean { + let enabled = true + this.stream.getAudioTracks().forEach(track => { + track.enabled = enabled = enabled && !track.enabled + }) + return enabled + } + + private onVideoTrackCb: ((t: MediaStreamTrack) => void) | null = null + onVideoTrack(cb: (t: MediaStreamTrack) => void) { + this.onVideoTrackCb = cb + } + + stop() { + this.stream.getTracks().forEach(t => t.stop()) + } +} + +export type LocalStream = InstanceType diff --git a/frontend/app/player/MessageDistributor/managers/StylesManager.js b/frontend/app/player/MessageDistributor/managers/StylesManager.ts similarity index 53% rename from frontend/app/player/MessageDistributor/managers/StylesManager.js rename to frontend/app/player/MessageDistributor/managers/StylesManager.ts index 9ae18fc83..389533368 100644 --- a/frontend/app/player/MessageDistributor/managers/StylesManager.js +++ b/frontend/app/player/MessageDistributor/managers/StylesManager.ts @@ -1,4 +1,3 @@ -// @flow import type StatedScreen from '../StatedScreen'; import type { CssInsertRule, CssDeleteRule } from '../messages'; @@ -10,21 +9,35 @@ type TimedCSSRuleMessage = Timed & CSSRuleMessage; import logger from 'App/logger'; import ListWalker from './ListWalker'; -export default class StylesManager extends ListWalker { - #screen: StatedScreen; - _linkLoadingCount: number = 0; - _linkLoadPromises: Array> = []; - _skipCSSLinks: Array = []; // should be common for all pages - constructor(screen: StatedScreen) { +const HOVER_CN = "-openreplay-hover"; +const HOVER_SELECTOR = `.${HOVER_CN}`; + +// Doesn't work with css files (hasOwnProperty) +export function rewriteNodeStyleSheet(doc: Document, node: HTMLLinkElement | HTMLStyleElement) { + const ss = Object.values(doc.styleSheets).find(s => s.ownerNode === node); + if (!ss || !ss.hasOwnProperty('rules')) { return; } + for(let i = 0; i < ss.rules.length; i++){ + const r = ss.rules[i] + if (r instanceof CSSStyleRule) { + r.selectorText = r.selectorText.replace('/\:hover/g', HOVER_SELECTOR) + } + } +} + +export default class StylesManager extends ListWalker { + private linkLoadingCount: number = 0; + private linkLoadPromises: Array> = []; + private skipCSSLinks: Array = []; // should be common for all pages + + constructor(private readonly screen: StatedScreen) { super(); - this.#screen = screen; } reset():void { super.reset(); - this._linkLoadingCount = 0; - this._linkLoadPromises = []; + this.linkLoadingCount = 0; + this.linkLoadPromises = []; //cancel all promises? tothinkaboutit } @@ -32,30 +45,34 @@ export default class StylesManager extends ListWalker { setStyleHandlers(node: HTMLLinkElement, value: string): void { let timeoutId; const promise = new Promise((resolve) => { - if (this._skipCSSLinks.includes(value)) resolve(); - this._linkLoadingCount++; - this.#screen.setCSSLoading(true); - const setSkipAndResolve = () => { - this._skipCSSLinks.push(value); // watch out - resolve(); + if (this.skipCSSLinks.includes(value)) resolve(null); + this.linkLoadingCount++; + this.screen.setCSSLoading(true); + const addSkipAndResolve = () => { + this.skipCSSLinks.push(value); // watch out + resolve(null); } - timeoutId = setTimeout(setSkipAndResolve, 4000); + timeoutId = setTimeout(addSkipAndResolve, 4000); - node.onload = resolve; - node.onerror = setSkipAndResolve; + node.onload = () => { + const doc = this.screen.document; + doc && rewriteNodeStyleSheet(doc, node); + resolve(null); + } + node.onerror = addSkipAndResolve; }).then(() => { node.onload = null; node.onerror = null; clearTimeout(timeoutId); - this._linkLoadingCount--; - if (this._linkLoadingCount === 0) { - this.#screen.setCSSLoading(false); + this.linkLoadingCount--; + if (this.linkLoadingCount === 0) { + this.screen.setCSSLoading(false); } }); - this._linkLoadPromises.push(promise); + this.linkLoadPromises.push(promise); } - #manageRule = (msg: CSSRuleMessage):void => { + private manageRule = (msg: CSSRuleMessage):void => { // if (msg.tp === "css_insert_rule") { // let styleSheet = this.#screen.document.styleSheets[ msg.stylesheetID ]; // if (!styleSheet) { @@ -86,7 +103,7 @@ export default class StylesManager extends ListWalker { } moveReady(t: number): Promise { - return Promise.all(this._linkLoadPromises) - .then(() => this.moveApply(t, this.#manageRule)); + return Promise.all(this.linkLoadPromises) + .then(() => this.moveApply(t, this.manageRule)); } } \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.js b/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.js index d3e0fd394..245ac9084 100644 --- a/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.js +++ b/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.js @@ -57,11 +57,12 @@ export default class WindowNodeCounter { addNode(id: number, parentID: number) { if (!this._nodes[ parentID ]) { - console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`); + //TODO: iframe case + //console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`); return; } if (!!this._nodes[ id ]) { - console.error(`Wrong! Node with id ${ id } already exists.`); + //console.error(`Wrong! Node with id ${ id } already exists.`); return; } this._nodes[id] = this._nodes[ parentID ].newChild(); diff --git a/frontend/app/player/MessageDistributor/messages.ts b/frontend/app/player/MessageDistributor/messages.ts index 92c7e60fb..6d9f3244c 100644 --- a/frontend/app/player/MessageDistributor/messages.ts +++ b/frontend/app/player/MessageDistributor/messages.ts @@ -36,6 +36,8 @@ export const ID_TP_MAP = { 54: "connection_information", 55: "set_page_visibility", 59: "long_task", + 69: "mouse_click", + 70: "create_i_frame_document", } as const; @@ -255,11 +257,26 @@ export interface LongTask { containerName: string, } +export interface MouseClick { + tp: "mouse_click", + id: number, + hesitationTime: number, + label: string, + selector: string, +} -export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask; +export interface CreateIFrameDocument { + tp: "create_i_frame_document", + frameID: number, + id: number, +} + + +export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask | MouseClick | CreateIFrameDocument; export default function (r: PrimitiveReader): Message | null { - switch (r.readUint()) { + const ui= r.readUint() + switch (ui) { case 0: return { @@ -509,7 +526,24 @@ export default function (r: PrimitiveReader): Message | null { containerName: r.readString(), }; + case 69: + return { + tp: ID_TP_MAP[69], + id: r.readUint(), + hesitationTime: r.readUint(), + label: r.readString(), + selector: r.readString(), + }; + + case 70: + return { + tp: ID_TP_MAP[70], + frameID: r.readUint(), + id: r.readUint(), + }; + default: + console.log("wtf is this", ui) r.readUint(); // IOS skip timestamp r.skip(r.readUint()); return null; diff --git a/frontend/app/types/filter/customFilter.js b/frontend/app/types/filter/customFilter.js index 950c1b61a..ea7d82af2 100644 --- a/frontend/app/types/filter/customFilter.js +++ b/frontend/app/types/filter/customFilter.js @@ -1,7 +1,7 @@ import Record from 'Types/Record'; import Target from 'Types/target'; import { camelCased } from 'App/utils'; -// import { getEventIcon } from 'Types/filter'; +import { getEventIcon } from 'Types/filter'; const CLICK = 'CLICK'; const INPUT = 'INPUT'; @@ -105,6 +105,6 @@ export default Record({ operator: event.operator || getOperatorDefault(event.type), // value: target ? target.label : event.value, value: typeof value === 'string' ? [value] : value, - icon: 'filters/metadata' + icon: event.type ? getEventIcon(event.type) : 'filters/metadata' }), }) diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index feaa44a20..42637a2fa 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -124,6 +124,7 @@ export const getEventIcon = (filter) => { type = type || key; if (type === KEYS.USER_COUNTRY) return 'map-marker-alt'; if (type === KEYS.USER_BROWSER) return 'window'; + if (type === KEYS.USERBROWSER) return 'window'; if (type === KEYS.PLATFORM) return 'window'; if (type === TYPES.CLICK) return 'filters/click'; diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js index 3926c1901..1fabc79a6 100644 --- a/frontend/app/types/session/session.js +++ b/frontend/app/types/session/session.js @@ -75,6 +75,7 @@ export default Record({ crashes: [], socket: null, isIOS: false, + revId: '' }, { fromJS:({ startTs=0, diff --git a/scripts/certbot.sh b/scripts/certbot.sh index 42cb00015..da5357ff8 100755 --- a/scripts/certbot.sh +++ b/scripts/certbot.sh @@ -14,7 +14,7 @@ read dns_name echo please enter your email id: read emai_id ssh_ansible_user=$(whoami) -certbot_home=/etc/letsencrypt/archive/$dns_name +certbot_home=/etc/letsencrypt/live/$dns_name #Check certbot installed or not @@ -26,8 +26,8 @@ fi sudo certbot certonly --non-interactive --agree-tos -m $emai_id -d $dns_name --standalone -sudo cp $certbot_home/privkey1.pem ${homedir}/site.key -sudo cp $certbot_home/fullchain1.pem ${homedir}/site.crt +sudo cp $certbot_home/privkey.pem ${homedir}/site.key +sudo cp $certbot_home/fullchain.pem ${homedir}/site.crt sudo chown -R $ssh_ansible_user:$ssh_ansible_user ${homedir}/site.key ${homedir}/site.crt sudo chmod 775 ${homedir}/site.crt ${homedir}/site.key diff --git a/scripts/helm/README.md b/scripts/helm/README.md index f4a842546..b3d618665 100644 --- a/scripts/helm/README.md +++ b/scripts/helm/README.md @@ -30,11 +30,11 @@ Installation components are separated by namespaces. **Scripts:** - **install.sh** - Installs OpenReplay in a single node machine, for trial runs / demo. + Installs OpenReplay in a single node machine. This script is a wrapper around the `install.sh` with [k3s](https://k3s.io/) as kubernetes distro. - Note: As of now this script support only ubuntu, as we've to install some packages to enable `NFS`. + Note: As of now this script support only Ubuntu, as we've to install some packages to enable `NFS`. - **kube-install.sh:** diff --git a/scripts/helm/app/chalice.yaml b/scripts/helm/app/chalice.yaml index 2a83f5deb..4ef1eecc7 100644 --- a/scripts/helm/app/chalice.yaml +++ b/scripts/helm/app/chalice.yaml @@ -54,8 +54,6 @@ env: sessions_bucket: mobs sourcemaps_bucket: sourcemaps js_cache_bucket: sessions-assets - sourcemaps_reader: 'http://utilities-openreplay.app.svc.cluster.local:9000/assist/sourcemaps' - peers: 'http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers' # Enable logging for python app # Ref: https://stackoverflow.com/questions/43969743/logs-in-kubernetes-pod-not-showing-up PYTHONUNBUFFERED: '0' diff --git a/scripts/helm/app/http.yaml b/scripts/helm/app/http.yaml index 5a9ae09ac..f594df201 100644 --- a/scripts/helm/app/http.yaml +++ b/scripts/helm/app/http.yaml @@ -29,7 +29,6 @@ env: AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" AWS_REGION: us-east-1 POSTGRES_STRING: postgres://postgres:asayerPostgres@postgresql.db.svc.cluster.local:5432 - CACHE_ASSETS: false # REDIS_STRING: redis-master.db.svc.cluster.local:6379 KAFKA_SERVERS: kafka.db.svc.cluster.local:9092 diff --git a/scripts/helm/app/openreplay/templates/deployment.yaml b/scripts/helm/app/openreplay/templates/deployment.yaml index a2259a852..187025b74 100644 --- a/scripts/helm/app/openreplay/templates/deployment.yaml +++ b/scripts/helm/app/openreplay/templates/deployment.yaml @@ -47,6 +47,17 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} {{- if .Values.pvc }} + {{- if eq .Values.pvc.name "hostPath" }} + volumeMounts: + - mountPath: {{ .Values.pvc.mountPath }} + name: {{ .Values.pvc.name }} + volumes: + - name: mydir + hostPath: + # Ensure the file directory is created. + path: {{ .Values.pvc.hostMountPath }} + type: DirectoryOrCreate + {{- else }} volumeMounts: - name: {{ .Values.pvc.name }} mountPath: {{ .Values.pvc.mountPath }} @@ -55,6 +66,7 @@ spec: persistentVolumeClaim: claimName: {{ .Values.pvc.volumeName }} {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/scripts/helm/app/openreplay/templates/pvc.yaml b/scripts/helm/app/openreplay/templates/pvc.yaml index 2180024e6..fc02fa6c3 100644 --- a/scripts/helm/app/openreplay/templates/pvc.yaml +++ b/scripts/helm/app/openreplay/templates/pvc.yaml @@ -1,5 +1,5 @@ {{- if .Values.pvc }} -{{- if .Values.pvc.create }} +{{- if and (.Values.pvc.create) (ne .Values.pvc.name "hostPath") }} apiVersion: v1 kind: PersistentVolumeClaim metadata: diff --git a/scripts/helm/app/sink.yaml b/scripts/helm/app/sink.yaml index 51113fdd0..8d6239d3e 100644 --- a/scripts/helm/app/sink.yaml +++ b/scripts/helm/app/sink.yaml @@ -23,10 +23,11 @@ resources: pvc: create: true - name: nfs + name: hostPath storageClassName: nfs volumeName: nfs mountPath: /mnt/efs + hostMountPath: /openreplay/storage/nfs storageSize: 5Gi env: diff --git a/scripts/helm/app/storage.yaml b/scripts/helm/app/storage.yaml index 836deab4a..ef839210e 100644 --- a/scripts/helm/app/storage.yaml +++ b/scripts/helm/app/storage.yaml @@ -24,10 +24,11 @@ resources: pvc: # PVC Created from filesink.yaml create: false - name: nfs + name: hostPath storageClassName: nfs volumeName: nfs mountPath: /mnt/efs + hostMountPath: /openreplay/storage/nfs storageSize: 5Gi env: @@ -43,3 +44,4 @@ env: KAFKA_SERVERS: kafka.db.svc.cluster.local:9092 KAFKA_USE_SSL: false LICENSE_KEY: "" + FS_CLEAN_HRS: 24 diff --git a/scripts/helm/db/init_dbs/postgresql/1.4.0/1.4.0.sql b/scripts/helm/db/init_dbs/postgresql/1.3.5/1.3.5.sql similarity index 100% rename from scripts/helm/db/init_dbs/postgresql/1.4.0/1.4.0.sql rename to scripts/helm/db/init_dbs/postgresql/1.3.5/1.3.5.sql diff --git a/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql b/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql index 50ce4bb0c..cc4704f26 100644 --- a/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql +++ b/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql @@ -4,7 +4,13 @@ CREATE INDEX sessions_user_id_useridNN_idx ON sessions (user_id) WHERE user_id I CREATE INDEX sessions_uid_projectid_startts_sessionid_uidNN_durGTZ_idx ON sessions (user_id, project_id, start_ts, session_id) WHERE user_id IS NOT NULL AND duration > 0; CREATE INDEX pages_base_path_base_pathLNGT2_idx ON events.pages (base_path) WHERE length(base_path) > 2; + CREATE INDEX clicks_session_id_timestamp_idx ON events.clicks (session_id, timestamp); CREATE INDEX errors_error_id_idx ON errors (error_id); CREATE INDEX errors_error_id_idx ON events.errors (error_id); + +CREATE INDEX issues_issue_id_timestamp_idx ON events_common.issues(issue_id,timestamp); +CREATE INDEX issues_timestamp_idx ON events_common.issues (timestamp); +CREATE INDEX issues_project_id_issue_id_idx ON public.issues (project_id, issue_id); + COMMIT; \ No newline at end of file diff --git a/scripts/helm/db/init_dbs/postgresql/init_schema.sql b/scripts/helm/db/init_dbs/postgresql/init_schema.sql index def953e2f..056e6b8c2 100644 --- a/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -172,7 +172,6 @@ CREATE TABLE projects "defaultInputMode": "plain" }'::jsonb -- ?????? ); -CREATE INDEX projects_tenant_id_idx ON projects (tenant_id); CREATE OR REPLACE FUNCTION notify_project() RETURNS trigger AS $$ @@ -248,7 +247,6 @@ create table webhooks index integer default 0 not null, name varchar(100) ); -CREATE INDEX webhooks_tenant_id_idx ON webhooks (tenant_id); -- --- notifications.sql --- diff --git a/scripts/helm/install.sh b/scripts/helm/install.sh index 881d98808..529d08a8c 100755 --- a/scripts/helm/install.sh +++ b/scripts/helm/install.sh @@ -31,7 +31,7 @@ which docker &> /dev/null || { # response {"data":{"valid": TRUE|FALSE, "expiration": expiration date in ms}} # Installing k3s -curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik --docker" sh - +curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik" sh - mkdir ~/.kube sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $(whoami) ~/.kube/config diff --git a/scripts/helm/nginx-ingress/nginx-ingress/README.md b/scripts/helm/nginx-ingress/nginx-ingress/README.md index 76c878b5a..89797d6e4 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/README.md +++ b/scripts/helm/nginx-ingress/nginx-ingress/README.md @@ -4,10 +4,10 @@ This is the frontend of the OpenReplay web app (internet). ## Endpoints -- /streaming -> ios-proxy - /api -> chalice -- /http -> http - / -> frontend (in minio) - /assets -> sessions-assets bucket in minio - /minio -> minio api endpoint - /ingest -> events ingestor +- /assist -> live sessions and webRTC +- /grafana -> monitoring (Enterprise Edition only) \ No newline at end of file diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml index fff5ac641..84cc6337d 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml @@ -52,14 +52,6 @@ data: proxy_set_header Host $host; proxy_pass $target; } - location /streaming/ { - set $target http://ios-proxy-openreplay.app.svc.cluster.local; rewrite ^/streaming/(.*) /$1 break; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Host $host; - proxy_pass $target; - } location /api/ { rewrite ^/api/(.*) /$1 break; proxy_http_version 1.1; diff --git a/scripts/helm/roles/openreplay/defaults/main.yaml b/scripts/helm/roles/openreplay/defaults/main.yaml index bcd303672..e706891fb 100644 --- a/scripts/helm/roles/openreplay/defaults/main.yaml +++ b/scripts/helm/roles/openreplay/defaults/main.yaml @@ -4,6 +4,6 @@ db_name: "" app_name: "" db_list: - "minio" - - "nfs-server-provisioner" - "postgresql" - "redis" + # - "nfs-server-provisioner" diff --git a/scripts/helm/roles/openreplay/templates/alerts.yaml b/scripts/helm/roles/openreplay/templates/alerts.yaml index 09f33c8bd..b28a73a53 100644 --- a/scripts/helm/roles/openreplay/templates/alerts.yaml +++ b/scripts/helm/roles/openreplay/templates/alerts.yaml @@ -5,6 +5,7 @@ image: {% endif %} env: LICENSE_KEY: "{{ enterprise_edition_license }}" + POSTGRES_STRING: "postgres://{{postgres_db_user}}:{{postgres_db_password}}@{{postgres_endpoint}}:{{postgres_port}}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/assets.yaml b/scripts/helm/roles/openreplay/templates/assets.yaml index 6383f4f2c..41f898260 100644 --- a/scripts/helm/roles/openreplay/templates/assets.yaml +++ b/scripts/helm/roles/openreplay/templates/assets.yaml @@ -7,6 +7,11 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" LICENSE_KEY: "{{ enterprise_edition_license }}" + AWS_ENDPOINT: "{{ s3_endpoint }}" + AWS_REGION: "{{ aws_region }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/chalice.yaml b/scripts/helm/roles/openreplay/templates/chalice.yaml index 3ab238e72..46de7488b 100644 --- a/scripts/helm/roles/openreplay/templates/chalice.yaml +++ b/scripts/helm/roles/openreplay/templates/chalice.yaml @@ -15,6 +15,22 @@ env: S3_HOST: "https://{{ domain_name }}" SITE_URL: "https://{{ domain_name }}" jwt_secret: "{{ jwt_secret_key }}" + pg_host: "{{ postgres_endpoint }}" + pg_port: "{{ postgres_port }}" + pg_dbname: "{{ postgres_port }}" + pg_user: "{{ postgres_db_user }}" + pg_password: "{{ postgres_db_password }}" + EMAIL_HOST: "{{ email_host }}" + EMAIL_PORT: "{{ email_port }}" + EMAIL_USER: "{{ email_user }}" + EMAIL_PASSWORD: "{{ email_password }}" + EMAIL_USE_TLS: "{{ email_use_tls }}" + EMAIL_USE_SSL: "{{ email_use_ssl }}" + EMAIL_SSL_KEY: "{{ email_ssl_key }}" + EMAIL_SSL_CERT: "{{ email_ssl_cert }}" + EMAIL_FROM: "{{ email_from }}" + AWS_DEFAULT_REGION: "{{ aws_default_region }}" + sessions_region: "{{ aws_default_region }}" {% if env is defined and env.chalice is defined and env.chalice%} {{ env.chalice | to_nice_yaml | trim | indent(2) }} {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/db.yaml b/scripts/helm/roles/openreplay/templates/db.yaml index ab8609111..7456794c8 100644 --- a/scripts/helm/roles/openreplay/templates/db.yaml +++ b/scripts/helm/roles/openreplay/templates/db.yaml @@ -5,6 +5,10 @@ image: {% endif %} env: LICENSE_KEY: "{{ enterprise_edition_license }}" + POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/ender.yaml b/scripts/helm/roles/openreplay/templates/ender.yaml index 5749e4a52..b5d256b2d 100644 --- a/scripts/helm/roles/openreplay/templates/ender.yaml +++ b/scripts/helm/roles/openreplay/templates/ender.yaml @@ -5,6 +5,9 @@ image: {% endif %} env: LICENSE_KEY: "{{ enterprise_edition_license }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/http.yaml b/scripts/helm/roles/openreplay/templates/http.yaml index 6383f4f2c..da7b0979f 100644 --- a/scripts/helm/roles/openreplay/templates/http.yaml +++ b/scripts/helm/roles/openreplay/templates/http.yaml @@ -7,6 +7,11 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" LICENSE_KEY: "{{ enterprise_edition_license }}" + AWS_REGION: "{{ aws_region }}" + POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/integrations.yaml b/scripts/helm/roles/openreplay/templates/integrations.yaml index 5749e4a52..9cc8f8b76 100644 --- a/scripts/helm/roles/openreplay/templates/integrations.yaml +++ b/scripts/helm/roles/openreplay/templates/integrations.yaml @@ -5,6 +5,11 @@ image: {% endif %} env: LICENSE_KEY: "{{ enterprise_edition_license }}" + POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}" + # + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/sink.yaml b/scripts/helm/roles/openreplay/templates/sink.yaml index 5749e4a52..b5d256b2d 100644 --- a/scripts/helm/roles/openreplay/templates/sink.yaml +++ b/scripts/helm/roles/openreplay/templates/sink.yaml @@ -5,6 +5,9 @@ image: {% endif %} env: LICENSE_KEY: "{{ enterprise_edition_license }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/storage.yaml b/scripts/helm/roles/openreplay/templates/storage.yaml index 6383f4f2c..6a70f3a4c 100644 --- a/scripts/helm/roles/openreplay/templates/storage.yaml +++ b/scripts/helm/roles/openreplay/templates/storage.yaml @@ -7,6 +7,12 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" LICENSE_KEY: "{{ enterprise_edition_license }}" + AWS_ENDPOINT: "{{ s3_endpoint }}" + AWS_REGION_WEB: "{{ aws_region }}" + AWS_REGION_IOS: "{{ aws_region }}" + REDIS_STRING: "{{ redis_endpoint }}" + KAFKA_SERVERS: "{{ kafka_endpoint }}" + KAFKA_USE_SSL: "{{ kafka_ssl }}" {% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] diff --git a/scripts/helm/roles/openreplay/templates/utilities.yaml b/scripts/helm/roles/openreplay/templates/utilities.yaml index 3ae1efca8..fb7eb0ae0 100644 --- a/scripts/helm/roles/openreplay/templates/utilities.yaml +++ b/scripts/helm/roles/openreplay/templates/utilities.yaml @@ -12,6 +12,7 @@ env: S3_SECRET: "{{ minio_secret_key }}" S3_HOST: "https://{{ domain_name }}" jwt_secret: "{{ jwt_secret_key }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" {% if env is defined and env.chalice is defined and env.chalice%} {{ env.chalice | to_nice_yaml | trim | indent(2) }} {% endif %} diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index 710fbd496..ca0037b27 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -86,3 +86,24 @@ db_resource_override: # memory: 256Mi redis: {} clickhouse: {} + +## Sane defaults +s3_endpoint: "http://minio.db.svc.cluster.local:9000" +aws_region: "us-east-1" +kafka_endpoint: kafka.db.svc.cluster.local:9042 +kafka_ssl: false +postgres_endpoint: postgresql.db.svc.cluster.local +postgres_port: 5432 +postgres_db_name: postgres +postgres_db_user: postgres +postgres_db_password: asayerPostgres +redis_endpoint: redis-master.db.svc.cluster.local:6379 +email_host: '' +email_port: '587' +email_user: '' +email_password: '' +email_use_tls: 'true' +email_use_ssl: 'false' +email_ssl_key: '' +email_ssl_cert: '' +email_from: OpenReplay diff --git a/scripts/helm/vars_template.yaml b/scripts/helm/vars_template.yaml index 6d92b8a66..766ed6a02 100644 --- a/scripts/helm/vars_template.yaml +++ b/scripts/helm/vars_template.yaml @@ -86,3 +86,24 @@ db_resource_override: # memory: 256Mi redis: {{ db_resource_override.redis|default({}) }} clickhouse: {{ db_resource_override.clickhouse|default({}) }} + +## Sane defaults +s3_endpoint: "{{ s3_endpoint }}" +aws_region: "{{ aws_region }}" +kafka_endpoint: "{{ kafka_endpoint }}" +kafka_ssl: "{{ kafka_ssl }}" +postgres_endpoint: "{{ postgres_endpoint }}" +postgres_port: "{{ postgres_port }}" +postgres_db_name: "{{ postgres_db_name }}" +postgres_db_user: "{{ postgres_db_user }}" +postgres_db_password: "{{ postgres_db_password }}" +redis_endpoint: "{{ redis_endpoint }}" +email_host: "{{ email_host }}" +email_port: "{{ email_port }}" +email_user: "{{ email_user }}" +email_password: "{{ email_password }}" +email_use_tls: "{{ email_use_tls }}" +email_use_ssl: "{{ email_use_ssl }}" +email_ssl_key: "{{ email_ssl_key }}" +email_ssl_cert: "{{ email_ssl_cert }}" +email_from: "{{ email_from }}" diff --git a/sourcemap-uploader/.eslintrc.json b/sourcemap-uploader/.eslintrc.json index fb4d5e7ee..0cad97a27 100644 --- a/sourcemap-uploader/.eslintrc.json +++ b/sourcemap-uploader/.eslintrc.json @@ -8,7 +8,9 @@ "module": true, "console": true, "Promise": true, - "Buffer": true + "Buffer": true, + "URL": true, + "global": true }, "plugins": [ "prettier" diff --git a/sourcemap-uploader/cli.js b/sourcemap-uploader/cli.js index 9dd3f16f3..7085f1345 100755 --- a/sourcemap-uploader/cli.js +++ b/sourcemap-uploader/cli.js @@ -14,7 +14,8 @@ parser.addArgument(['-k', '--api-key'], { help: 'API key', required: true, }); -parser.addArgument(['-p', '-i', '--project-key'], { // -i is depricated +parser.addArgument(['-p', '-i', '--project-key'], { + // -i is depricated help: 'Project Key', required: true, }); @@ -54,19 +55,34 @@ dir.addArgument(['-u', '--js-dir-url'], { // TODO: exclude in dir -const { command, api_key, project_key, server, verbose, ...args } = parser.parseArgs(); +const { command, api_key, project_key, server, verbose, ...args } = + parser.parseArgs(); global._VERBOSE = !!verbose; (command === 'file' - ? uploadFile(api_key, project_key, args.sourcemap_file_path, args.js_file_url, server) - : uploadDir(api_key, project_key, args.sourcemap_dir_path, args.js_dir_url, server) + ? uploadFile( + api_key, + project_key, + args.sourcemap_file_path, + args.js_file_url, + server, + ) + : uploadDir( + api_key, + project_key, + args.sourcemap_dir_path, + args.js_dir_url, + server, + ) ) -.then((sourceFiles) => - sourceFiles.length > 0 - ? console.log(`Successfully uploaded ${sourceFiles.length} sourcemap file${sourceFiles.length > 1 ? "s" : ""} for: \n` - + sourceFiles.join("\t\n") + .then((sourceFiles) => + sourceFiles.length > 0 + ? console.log( + `Successfully uploaded ${sourceFiles.length} sourcemap file${ + sourceFiles.length > 1 ? 's' : '' + } for: \n` + sourceFiles.join('\t\n'), + ) + : console.log(`No sourcemaps found in ${args.sourcemap_dir_path}`), ) - : console.log(`No sourcemaps found in ${ args.sourcemap_dir_path }`) -) -.catch(e => console.error(`Sourcemap Uploader: ${e}`)); + .catch((e) => console.error(`Sourcemap Uploader: ${e}`)); diff --git a/sourcemap-uploader/index.js b/sourcemap-uploader/index.js index 7c3ee7aab..dabbee434 100644 --- a/sourcemap-uploader/index.js +++ b/sourcemap-uploader/index.js @@ -3,11 +3,23 @@ const readFile = require('./lib/readFile.js'), uploadSourcemaps = require('./lib/uploadSourcemaps.js'); module.exports = { - async uploadFile(api_key, project_key, sourcemap_file_path, js_file_url, server) { + async uploadFile( + api_key, + project_key, + sourcemap_file_path, + js_file_url, + server, + ) { const sourcemap = await readFile(sourcemap_file_path, js_file_url); return uploadSourcemaps(api_key, project_key, [sourcemap], server); }, - async uploadDir(api_key, project_key, sourcemap_dir_path, js_dir_url, server) { + async uploadDir( + api_key, + project_key, + sourcemap_dir_path, + js_dir_url, + server, + ) { const sourcemaps = await readDir(sourcemap_dir_path, js_dir_url); return uploadSourcemaps(api_key, project_key, sourcemaps, server); }, diff --git a/sourcemap-uploader/lib/readDir.js b/sourcemap-uploader/lib/readDir.js index 501a2949f..bb7825964 100644 --- a/sourcemap-uploader/lib/readDir.js +++ b/sourcemap-uploader/lib/readDir.js @@ -3,12 +3,13 @@ const readFile = require('./readFile'); module.exports = (sourcemap_dir_path, js_dir_url) => { sourcemap_dir_path = (sourcemap_dir_path + '/').replace(/\/+/g, '/'); - if (js_dir_url[ js_dir_url.length - 1 ] !== '/') { // replace will break schema + if (js_dir_url[js_dir_url.length - 1] !== '/') { + // replace will break schema js_dir_url += '/'; } - return glob(sourcemap_dir_path + '**/*.map').then(sourcemap_file_paths => + return glob(sourcemap_dir_path + '**/*.map').then((sourcemap_file_paths) => Promise.all( - sourcemap_file_paths.map(sourcemap_file_path => + sourcemap_file_paths.map((sourcemap_file_path) => readFile( sourcemap_file_path, js_dir_url + sourcemap_file_path.slice(sourcemap_dir_path.length, -4), diff --git a/sourcemap-uploader/lib/readFile.js b/sourcemap-uploader/lib/readFile.js index 67cec2f82..cca02206b 100644 --- a/sourcemap-uploader/lib/readFile.js +++ b/sourcemap-uploader/lib/readFile.js @@ -1,6 +1,6 @@ const fs = require('fs').promises; module.exports = (sourcemap_file_path, js_file_url) => - fs.readFile(sourcemap_file_path, 'utf8').then(body => { + fs.readFile(sourcemap_file_path, 'utf8').then((body) => { return { sourcemap_file_path, js_file_url, body }; }); diff --git a/sourcemap-uploader/lib/uploadSourcemaps.js b/sourcemap-uploader/lib/uploadSourcemaps.js index 8da34ecdc..5bfc8a0f4 100644 --- a/sourcemap-uploader/lib/uploadSourcemaps.js +++ b/sourcemap-uploader/lib/uploadSourcemaps.js @@ -9,43 +9,47 @@ const getUploadURLs = (api_key, project_key, js_file_urls, server) => let serverURL; try { serverURL = new URL(server); - } catch(e) { - return reject(`Failed to parse server URL "${server}".`) + } catch (e) { + return reject(`Failed to parse server URL "${server}".`); } - const pathPrefix = (serverURL.pathname + "/").replace(/\/+/g, '/'); + const pathPrefix = (serverURL.pathname + '/').replace(/\/+/g, '/'); const options = { method: 'PUT', hostname: serverURL.host, path: pathPrefix + `${project_key}/sourcemaps/`, headers: { Authorization: api_key, 'Content-Type': 'application/json' }, - } + }; if (global._VERBOSE) { - console.log("Request: ", options, "\nFiles: ", js_file_urls); + console.log('Request: ', options, '\nFiles: ', js_file_urls); } - const req = https.request( - options, - res => { + const req = https.request(options, (res) => { + if (global._VERBOSE) { + console.log( + 'Response Code: ', + res.statusCode, + '\nMessage: ', + res.statusMessage, + ); + } + if (res.statusCode === 403) { + reject( + 'Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.', + ); + return; + } else if (res.statusCode !== 200) { + reject('Server Error. Please, contact OpenReplay support.'); + return; + } + let data = ''; + res.on('data', (s) => (data += s)); + res.on('end', () => { if (global._VERBOSE) { - console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); + console.log('Server Response: ', data); } - if (res.statusCode === 403) { - reject("Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.") - return - } else if (res.statusCode !== 200) { - reject("Server Error. Please, contact OpenReplay support."); - return; - } - let data = ''; - res.on('data', s => (data += s)); - res.on('end', () => { - if (global._VERBOSE) { - console.log("Server Response: ", data) - } - resolve(JSON.parse(data).data) - }); - }, - ); + resolve(JSON.parse(data).data); + }); + }); req.on('error', reject); req.write(JSON.stringify({ URL: js_file_urls })); req.end(); @@ -63,14 +67,19 @@ const uploadSourcemap = (upload_url, body) => 'Content-Type': 'application/json', }, }, - res => { + (res) => { if (res.statusCode !== 200) { if (global._VERBOSE) { - console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); + console.log( + 'Response Code: ', + res.statusCode, + '\nMessage: ', + res.statusMessage, + ); } - reject("Unable to upload. Please, contact OpenReplay support."); - return; // TODO: report per-file errors. + reject('Unable to upload. Please, contact OpenReplay support.'); + return; // TODO: report per-file errors. } resolve(); //res.on('end', resolve); @@ -86,12 +95,13 @@ module.exports = (api_key, project_key, sourcemaps, server) => api_key, project_key, sourcemaps.map(({ js_file_url }) => js_file_url), - server || "https://api.openreplay.com", - ).then(upload_urls => + server || 'https://api.openreplay.com', + ).then((upload_urls) => Promise.all( upload_urls.map((upload_url, i) => - uploadSourcemap(upload_url, sourcemaps[i].body) - .then(() => sourcemaps[i].js_file_url) + uploadSourcemap(upload_url, sourcemaps[i].body).then( + () => sourcemaps[i].js_file_url, + ), ), ), ); diff --git a/sourcemap-uploader/package-lock.json b/sourcemap-uploader/package-lock.json index 40b676c79..0b38bb293 100644 --- a/sourcemap-uploader/package-lock.json +++ b/sourcemap-uploader/package-lock.json @@ -1,29 +1,88 @@ { "name": "@openreplay/sourcemap-uploader", - "version": "3.0.1", + "version": "3.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } } }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -50,21 +109,21 @@ "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" }, "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -73,19 +132,16 @@ "uri-js": "^4.2.2" } }, - "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -106,15 +162,15 @@ } }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "brace-expansion": { @@ -134,37 +190,56 @@ "dev": true }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -187,39 +262,29 @@ "dev": true }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "doctrine": { @@ -237,109 +302,134 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" } }, "eslint-config-prettier": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz", - "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", + "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", + "dev": true }, "eslint-plugin-prettier": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", - "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", + "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0" } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "esprima": { @@ -349,21 +439,37 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -378,21 +484,10 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-diff": { @@ -413,39 +508,29 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, "fs.realpath": { @@ -460,16 +545,10 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -481,9 +560,9 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -498,12 +577,12 @@ } }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" } }, "has-flag": { @@ -512,15 +591,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -528,9 +598,9 @@ "dev": true }, "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -559,27 +629,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -601,12 +650,6 @@ "is-extglob": "^2.1.1" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -620,9 +663,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -642,27 +685,42 @@ "dev": true }, "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -672,45 +730,18 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -720,35 +751,20 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -765,21 +781,21 @@ "dev": true }, "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", "dev": true }, "prettier-linter-helpers": { @@ -804,9 +820,15 @@ "dev": true }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "resolve-from": { @@ -815,91 +837,72 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" } }, "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true } } @@ -910,48 +913,29 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.1" } }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { @@ -964,39 +948,36 @@ } }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true } } }, @@ -1006,61 +987,40 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "^1.2.1" } }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { "punycode": "^2.1.0" } }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -1078,14 +1038,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } } diff --git a/sourcemap-uploader/package.json b/sourcemap-uploader/package.json index 4a1d20181..e8da522a1 100644 --- a/sourcemap-uploader/package.json +++ b/sourcemap-uploader/package.json @@ -13,10 +13,10 @@ ], "license": "MIT", "devDependencies": { - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.10.0", - "eslint-plugin-prettier": "^3.1.2", - "prettier": "^1.19.1" + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "prettier": "^2.4.1" }, "dependencies": { "argparse": "^1.0.10", diff --git a/tracker/tracker-assist/.npmignore b/tracker/tracker-assist/.npmignore index a3e81897a..038eacb24 100644 --- a/tracker/tracker-assist/.npmignore +++ b/tracker/tracker-assist/.npmignore @@ -3,3 +3,4 @@ tsconfig-cjs.json tsconfig.json .prettierrc.json .cache +layout diff --git a/tracker/tracker-assist/layout/css/bootstrap.min.css b/tracker/tracker-assist/layout/css/bootstrap.min.css new file mode 100644 index 000000000..376294960 --- /dev/null +++ b/tracker/tracker-assist/layout/css/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x)/ -2);margin-left:calc(var(--bs-gutter-x)/ -2)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)/ 2);padding-left:calc(var(--bs-gutter-x)/ 2);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.3333333333%}.col-xxl-2{flex:0 0 auto;width:16.6666666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.3333333333%}.col-xxl-5{flex:0 0 auto;width:41.6666666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.3333333333%}.col-xxl-8{flex:0 0 auto;width:66.6666666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.3333333333%}.col-xxl-11{flex:0 0 auto;width:91.6666666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.3333333333%}.offset-xxl-2{margin-left:16.6666666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.3333333333%}.offset-xxl-5{margin-left:41.6666666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.3333333333%}.offset-xxl-8{margin-left:66.6666666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.3333333333%}.offset-xxl-11{margin-left:91.6666666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);padding:1rem .75rem}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754;padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545;padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu{top:0;right:auto;left:100%}.dropend .dropdown-menu[data-bs-popper]{margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu{top:0;right:100%;left:auto}.dropstart .dropdown-menu[data-bs-popper]{margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:last-of-type{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid #d8d8d8;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1040;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.offcanvas-backdrop::before{position:fixed;top:0;left:0;z-index:1039;width:100vw;height:100vh;content:"";background-color:rgba(0,0,0,.5)}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ diff --git a/tracker/tracker-assist/layout/css/styles.css b/tracker/tracker-assist/layout/css/styles.css new file mode 100644 index 000000000..289cddc17 --- /dev/null +++ b/tracker/tracker-assist/layout/css/styles.css @@ -0,0 +1,86 @@ +.connecting-message { + margin-top: 50%; + font-size: 20px; + color: #aaa; + text-align: center; + display: none; +} + +.status-connecting .connecting-message { + display: block; +} +.status-connecting .card { + display: none; +} + +.card{ + min-width: 324px; + width: 350px; + max-width: 800px; + /*min-height: 220px;*/ + max-height: 450px; + /*resize: both; + overflow: auto;*/ +} + +.card .card-header{ + cursor: move; +} +#agent-name, #duration{ + cursor:default; +} + +#local-stream, #remote-stream { + display:none; +} +#video-container.remote #remote-stream { + display: block; +} +#video-container.local { + min-height: 100px; +} +#video-container.local #local-stream { + display: block; +} + +#local-stream{ + width: 35%; + position: absolute; + z-index: 99; + bottom: 5px; + right: 5px; + border: thin solid rgba(255,255,255, .3); +} + + +#audio-btn .bi-mic-mute { + display:none; +} +#audio-btn:after { + content: 'Mute' +} +#audio-btn.muted .bi-mic-mute { + display: inline-block; +} +#audio-btn.muted .bi-mic { + display:none; +} +#audio-btn.muted:after { + content: 'Unmute' +} + +#video-btn .bi-camera-video-off { + display:none; +} +#video-btn.off:after { + content: 'Start Video' +} +#video-btn.off .bi-camera-video-off { + display: inline-block; +} +#video-btn.off .bi-camera-video { + display:none; +} +#video-btn:after { + content: 'Stop Video' +} \ No newline at end of file diff --git a/tracker/tracker-assist/layout/index.html b/tracker/tracker-assist/layout/index.html new file mode 100644 index 000000000..bc323b16c --- /dev/null +++ b/tracker/tracker-assist/layout/index.html @@ -0,0 +1,168 @@ + + + + + + + OpenReplay | Assist + + + + + + + + + + +
+
Connecting...
+
+
+ +
+ + 00:00 +
+
+
+
+

Starting video...

+ +
+ +
+

Starting video...

+ +
+
+ + +
+
+ + \ No newline at end of file diff --git a/tracker/tracker-assist/package-lock.json b/tracker/tracker-assist/package-lock.json index d797a86d4..8781de1bf 100644 --- a/tracker/tracker-assist/package-lock.json +++ b/tracker/tracker-assist/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openreplay/tracker-assist", - "version": "3.1.0", + "version": "3.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -63,9 +63,9 @@ } }, "@openreplay/tracker": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@openreplay/tracker/-/tracker-3.3.0.tgz", - "integrity": "sha512-g9sOG01VaiRLw4TcUbux8j3moa7gsGs8rjZPEVJ5SJqxjje9R7tpUD5UId9ne7QdHSoiHfrWFk3TNRLpXyvImg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@openreplay/tracker/-/tracker-3.4.4.tgz", + "integrity": "sha512-IcuxwwTt1RtLZw9QlQVAVNqoybv0ZkD2ZDk2FeHEQ/+BItsMhG61/4/lB2yXKLTLr6ydeKTzwYvxfr1vwxn2dw==", "dev": true, "requires": { "@medv/finder": "^2.0.0", diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 683c9c322..b9d4b77ef 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker-assist", "description": "Tracker plugin for screen assistance through the WebRTC", - "version": "3.1.1", + "version": "3.4.4", "keywords": [ "WebRTC", "assistance", @@ -24,10 +24,10 @@ "peerjs": "^1.3.2" }, "peerDependencies": { - "@openreplay/tracker": "^3.3.0" + "@openreplay/tracker": "^3.4.3" }, "devDependencies": { - "@openreplay/tracker": "^3.3.0", + "@openreplay/tracker": "^3.4.3", "prettier": "^1.18.2", "replace-in-files-cli": "^1.0.0", "typescript": "^3.6.4" diff --git a/tracker/tracker-assist/src/BufferingConnection.ts b/tracker/tracker-assist/src/BufferingConnection.ts new file mode 100644 index 000000000..766bd7892 --- /dev/null +++ b/tracker/tracker-assist/src/BufferingConnection.ts @@ -0,0 +1,36 @@ +import type { DataConnection } from 'peerjs'; + +// TODO: proper Message type export from tracker in 3.5.0 +interface Message { + encode(w: any): boolean; +} + +// Bffering required in case of webRTC +export default class BufferingConnection { + private readonly buffer: Message[][] = [] + private buffering: boolean = false + + constructor(readonly conn: DataConnection){} + private sendNext() { + if (this.buffer.length) { + setTimeout(() => { + this.conn.send(this.buffer.shift()) + this.sendNext() + }, 50) + } else { + this.buffering = false + } + } + + send(messages: Message[]) { + if (!this.conn.open) { return; } + let i = 0; + while (i < messages.length) { + this.buffer.push(messages.slice(i, i+=1000)) + } + if (!this.buffering) { + this.buffering = true + this.sendNext(); + } + } +} \ No newline at end of file diff --git a/tracker/tracker-assist/src/CallWindow.ts b/tracker/tracker-assist/src/CallWindow.ts index 860753aa7..3356449ce 100644 --- a/tracker/tracker-assist/src/CallWindow.ts +++ b/tracker/tracker-assist/src/CallWindow.ts @@ -1,15 +1,23 @@ +import type { LocalStream } from './LocalStream'; +const SS_START_TS_KEY = "__openreplay_assist_call_start_ts" export default class CallWindow { - private iframe: HTMLIFrameElement; - private vRemote: HTMLVideoElement | null = null; - private vLocal: HTMLVideoElement | null = null; - private audioBtn: HTMLAnchorElement | null = null; - private videoBtn: HTMLAnchorElement | null = null; - private userNameSpan: HTMLSpanElement | null = null; + private iframe: HTMLIFrameElement + private vRemote: HTMLVideoElement | null = null + private vLocal: HTMLVideoElement | null = null + private audioBtn: HTMLElement | null = null + private videoBtn: HTMLElement | null = null + private endCallBtn: HTMLElement | null = null + private agentNameElem: HTMLElement | null = null + private videoContainer: HTMLElement | null = null + private vPlaceholder: HTMLElement | null = null - private tsInterval: ReturnType; - constructor(endCall: () => void) { + private tsInterval: ReturnType + + private load: Promise + + constructor() { const iframe = this.iframe = document.createElement('iframe'); Object.assign(iframe.style, { position: "fixed", @@ -23,188 +31,228 @@ export default class CallWindow { height: "200px", width: "200px", }); - //iframe.src = "//static.openreplay.com/tracker-assist/index.html"; - iframe.onload = () => { - const doc = iframe.contentDocument; - if (!doc) { - console.error("OpenReplay: CallWindow iframe document is not reachable.") - return; - } - fetch("https://static.openreplay.com/tracker-assist/index.html") - //fetch("file:///Users/shikhu/work/asayer-tester/dist/assist/index.html") - .then(r => r.text()) - .then((text) => { - iframe.onload = () => { - doc.body.removeChild(doc.body.children[0]); //?!!>R# - const assistSection = doc.getElementById("or-assist") - assistSection && assistSection.removeAttribute("style"); - iframe.style.height = doc.body.scrollHeight + 'px'; - iframe.style.width = doc.body.scrollWidth + 'px'; - iframe.onload = null; - } - - text = text.replace(/href="css/g, "href=\"https://static.openreplay.com/tracker-assist/css") - doc.open(); - doc.write(text); - doc.close(); - - - this.vLocal = doc.getElementById("video-local") as HTMLVideoElement; - this.vRemote = doc.getElementById("video-remote") as HTMLVideoElement; - this._trySetStreams(); - // - this.vLocal.parentElement && this.vLocal.parentElement.classList.add("d-none"); - - this.audioBtn = doc.getElementById("audio-btn") as HTMLAnchorElement; - this.audioBtn.onclick = () => this.toggleAudio(); - this.videoBtn = doc.getElementById("video-btn") as HTMLAnchorElement; - this.videoBtn.onclick = () => this.toggleVideo(); - - this.userNameSpan = doc.getElementById("username") as HTMLSpanElement; - this._trySetAssistentName(); - - const endCallBtn = doc.getElementById("end-call-btn") as HTMLAnchorElement; - endCallBtn.onclick = endCall; - - const tsText = doc.getElementById("time-stamp"); - const startTs = Date.now(); - if (tsText) { - this.tsInterval = setInterval(() => { - const ellapsed = Date.now() - startTs; - const secsFull = ~~(ellapsed / 1000); - const mins = ~~(secsFull / 60); - const secs = secsFull - mins * 60 - tsText.innerText = `${mins}:${secs < 10 ? 0 : ''}${secs}`; - }, 500); - } - - // TODO: better D'n'D - doc.body.setAttribute("draggable", "true"); - doc.body.ondragstart = (e) => { - if (!e.dataTransfer || !e.target) { return; } - //@ts-ignore - if (!e.target.classList || !e.target.classList.contains("card-header")) { return; } - e.dataTransfer.setDragImage(doc.body, e.clientX, e.clientY); - }; - doc.body.ondragend = e => { - Object.assign(iframe.style, { - left: `${e.clientX}px`, - top: `${e.clientY}px`, - bottom: 'auto', - right: 'auto', - }) - } - }); - } - document.body.appendChild(iframe); + const doc = iframe.contentDocument; + if (!doc) { + console.error("OpenReplay: CallWindow iframe document is not reachable.") + return; + } + + + //const baseHref = "https://static.openreplay.com/tracker-assist/test" + const baseHref = "https://static.openreplay.com/tracker-assist/3.4.4" + this.load = fetch(baseHref + "/index.html") + .then(r => r.text()) + .then((text) => { + iframe.onload = () => { + const assistSection = doc.getElementById("or-assist") + assistSection?.classList.remove("status-connecting") + //iframe.style.height = doc.body.scrollHeight + 'px'; + //iframe.style.width = doc.body.scrollWidth + 'px'; + this.adjustIframeSize() + iframe.onload = null; + } + + // ? + text = text.replace(/href="css/g, `href="${baseHref}/css`) + doc.open(); + doc.write(text); + doc.close(); + + + this.vLocal = doc.getElementById("video-local") as (HTMLVideoElement | null); + this.vRemote = doc.getElementById("video-remote") as (HTMLVideoElement | null); + this.videoContainer = doc.getElementById("video-container"); + + this.audioBtn = doc.getElementById("audio-btn"); + if (this.audioBtn) { + this.audioBtn.onclick = () => this.toggleAudio(); + } + this.videoBtn = doc.getElementById("video-btn"); + if (this.videoBtn) { + this.videoBtn.onclick = () => this.toggleVideo(); + } + this.endCallBtn = doc.getElementById("end-call-btn"); + + this.agentNameElem = doc.getElementById("agent-name"); + this.vPlaceholder = doc.querySelector("#remote-stream p") + + const tsElem = doc.getElementById("duration"); + if (tsElem) { + const startTs = Number(sessionStorage.getItem(SS_START_TS_KEY)) || Date.now() + sessionStorage.setItem(SS_START_TS_KEY, startTs.toString()) + this.tsInterval = setInterval(() => { + const ellapsed = Date.now() - startTs + const secsFull = ~~(ellapsed / 1000) + const mins = ~~(secsFull / 60) + const secs = secsFull - mins * 60 + tsElem.innerText = `${mins}:${secs < 10 ? 0 : ''}${secs}` + }, 500); + } + + // TODO: better D'n'D + // mb set cursor:move here? + doc.body.setAttribute("draggable", "true"); + doc.body.ondragstart = (e) => { + if (!e.dataTransfer || !e.target) { return; } + //@ts-ignore + if (!e.target.classList || !e.target.classList.contains("drag-area")) { return; } + e.dataTransfer.setDragImage(doc.body, e.clientX, e.clientY); + }; + doc.body.ondragend = e => { + Object.assign(iframe.style, { + left: `${e.clientX}px`, // TODO: fix the case when ecoordinates are inside the iframe + top: `${e.clientY}px`, + bottom: 'auto', + right: 'auto', + }) + } + }); + + //this.toggleVideoUI(false) + //this.toggleRemoteVideoUI(false) } - private aRemote: HTMLAudioElement | null = null; - private localStream: MediaStream | null = null; - private remoteStream: MediaStream | null = null; - private setLocalVideoStream: (MediaStream) => void = () => {}; - private videoRequested: boolean = true; // TODO: green camera light - private _trySetStreams() { - if (this.vRemote && !this.vRemote.srcObject && this.remoteStream) { - this.vRemote.srcObject = this.remoteStream; - // Hack for audio (doesen't work in iframe because of some magical reasons) - this.aRemote = document.createElement("audio"); - this.aRemote.autoplay = true; - this.aRemote.style.display = "none" - this.aRemote.srcObject = this.remoteStream; - document.body.appendChild(this.aRemote) - } - if (this.vLocal && !this.vLocal.srcObject && this.localStream) { - this.vLocal.srcObject = this.localStream; - } + private adjustIframeSize() { + const doc = this.iframe.contentDocument + if (!doc) { return } + this.iframe.style.height = doc.body.scrollHeight + 'px'; + this.iframe.style.width = doc.body.scrollWidth + 'px'; } + + setCallEndAction(endCall: () => void) { + this.load.then(() => { + if (this.endCallBtn) { + this.endCallBtn.onclick = endCall + } + }) + } + + private aRemote: HTMLAudioElement | null = null; + private checkRemoteVideoInterval: ReturnType setRemoteStream(rStream: MediaStream) { - this.remoteStream = rStream; - this._trySetStreams(); - } - setLocalStream(lStream: MediaStream, setLocalVideoStream: (MediaStream) => void) { - lStream.getVideoTracks().forEach(track => { - track.enabled = false; - }); - this.localStream = lStream; - this.setLocalVideoStream = setLocalVideoStream; - this._trySetStreams(); + this.load.then(() => { + if (this.vRemote && !this.vRemote.srcObject) { + this.vRemote.srcObject = rStream; + if (this.vPlaceholder) { + this.vPlaceholder.innerText = "Video has been paused. Click anywhere to resume."; + } + + // Hack for audio. Doesen't work inside the iframe because of some magical reasons (check if it is connected to autoplay?) + this.aRemote = document.createElement("audio"); + this.aRemote.autoplay = true; + this.aRemote.style.display = "none" + this.aRemote.srcObject = rStream; + document.body.appendChild(this.aRemote) + } + + // Hack to determine if the remote video is enabled + if (this.checkRemoteVideoInterval) { clearInterval(this.checkRemoteVideoInterval) } // just in case + let enable = false + this.checkRemoteVideoInterval = setInterval(() => { + const settings = rStream.getVideoTracks()[0]?.getSettings() + //console.log(settings) + const isDummyVideoTrack = !!settings && (settings.width === 2 || settings.frameRate === 0) + const shouldEnable = !isDummyVideoTrack + if (enable !== shouldEnable) { + this.toggleRemoteVideoUI(enable=shouldEnable) + } + }, 1000) + }) } - - // TODO: determined workflow - _trySetAssistentName() { - if (this.userNameSpan && this.assistentName) { - this.userNameSpan.innerText = this.assistentName; - } + toggleRemoteVideoUI(enable: boolean) { + this.load.then(() => { + if (this.videoContainer) { + if (enable) { + this.videoContainer.classList.add("remote") + } else { + this.videoContainer.classList.remove("remote") + } + this.adjustIframeSize() + } + }) } - private assistentName: string = ""; + + private localStream: LocalStream | null = null; + + // TODO: on construction? + setLocalStream(lStream: LocalStream) { + this.localStream = lStream + } + + playRemote() { + this.vRemote && this.vRemote.play() + } + setAssistentName(name: string) { - this.assistentName = name; - this._trySetAssistentName(); + this.load.then(() => { + if (this.agentNameElem) { + this.agentNameElem.innerText = name + } + }) } - toggleAudio() { - let enabled = true; - this.localStream?.getAudioTracks().forEach(track => { - enabled = enabled && !track.enabled; - track.enabled = enabled; - }); - const cList = this.audioBtn?.classList; + + private toggleAudioUI(enabled: boolean) { if (!this.audioBtn) { return; } if (enabled) { - this.audioBtn.classList.remove("muted"); - this.audioBtn.childNodes[1].textContent = "Mute"; + this.audioBtn.classList.remove("muted") } else { - this.audioBtn.classList.add("muted"); - this.audioBtn.childNodes[1].textContent = "Unmute"; + this.audioBtn.classList.add("muted") } } - private _toggleVideoUI(enabled) { - if (!this.videoBtn || !this.vLocal || !this.vLocal.parentElement) { return; } + private toggleAudio() { + const enabled = this.localStream?.toggleAudio() || false + this.toggleAudioUI(enabled) + // if (!this.audioBtn) { return; } + // if (enabled) { + // this.audioBtn.classList.remove("muted"); + // this.audioBtn.childNodes[1].textContent = "Mute"; + // } else { + // this.audioBtn.classList.add("muted"); + // this.audioBtn.childNodes[1].textContent = "Unmute"; + // } + } + + private toggleVideoUI(enabled: boolean) { + if (!this.videoBtn || !this.videoContainer) { return; } if (enabled) { - this.vLocal.parentElement.classList.remove("d-none"); + this.videoContainer.classList.add("local") this.videoBtn.classList.remove("off"); - this.videoBtn.childNodes[1].textContent = "Stop Video"; } else { - this.vLocal.parentElement.classList.add("d-none"); + this.videoContainer.classList.remove("local") this.videoBtn.classList.add("off"); - this.videoBtn.childNodes[1].textContent = "Start Video"; } + this.adjustIframeSize() } - toggleVideo() { - if (!this.videoRequested) { - navigator.mediaDevices.getUserMedia({video:true, audio:false}).then(vd => { - this.videoRequested = true; - this.setLocalVideoStream(vd); - this._toggleVideoUI(true); - this.localStream?.getVideoTracks().forEach(track => { - track.enabled = true; - }) - }); - return; - } - let enabled = true; - this.localStream?.getVideoTracks().forEach(track => { - enabled = enabled && !track.enabled; - track.enabled = enabled; - }); - this._toggleVideoUI(enabled); - + private videoRequested: boolean = false + private toggleVideo() { + this.localStream?.toggleVideo() + .then(enabled => { + this.toggleVideoUI(enabled) + this.load.then(() => { + if (this.vLocal && this.localStream && !this.vLocal.srcObject) { + this.vLocal.srcObject = this.localStream.stream + } + }) + }) } remove() { - clearInterval(this.tsInterval); + this.localStream?.stop() + clearInterval(this.tsInterval) + clearInterval(this.checkRemoteVideoInterval) if (this.iframe.parentElement) { - document.body.removeChild(this.iframe); + document.body.removeChild(this.iframe) } if (this.aRemote && this.aRemote.parentElement) { - document.body.removeChild(this.aRemote); + document.body.removeChild(this.aRemote) } + sessionStorage.removeItem(SS_START_TS_KEY) } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/ConfirmWindow.ts b/tracker/tracker-assist/src/ConfirmWindow.ts index 64dc98a28..6257f4376 100644 --- a/tracker/tracker-assist/src/ConfirmWindow.ts +++ b/tracker/tracker-assist/src/ConfirmWindow.ts @@ -67,26 +67,32 @@ export default class ConfirmWindow { this.wrapper = wrapper; answerBtn.onclick = () => { - this.remove(); - this.callback(true); + this._remove(); + this.resolve(true); } declineBtn.onclick = () => { - this.remove(); - this.callback(false); + this._remove(); + this.resolve(false); } } - mount() { + private resolve: (result: boolean) => void = ()=>{}; + private reject: ()=>void = ()=>{}; + + mount(): Promise { document.body.appendChild(this.wrapper); + return new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); } - private callback: (result: boolean) => void = ()=>{}; - onAnswer(callback: (result: boolean) => void) { - this.callback = callback; - } - - remove() { + private _remove() { if (!this.wrapper.parentElement) { return; } document.body.removeChild(this.wrapper); } + remove() { + this._remove(); + this.reject(); + } } diff --git a/tracker/tracker-assist/src/LocalStream.ts b/tracker/tracker-assist/src/LocalStream.ts new file mode 100644 index 000000000..63f01ad58 --- /dev/null +++ b/tracker/tracker-assist/src/LocalStream.ts @@ -0,0 +1,85 @@ +declare global { + interface HTMLCanvasElement { + captureStream(frameRate?: number): MediaStream; + } +} + +function dummyTrack(): MediaStreamTrack { + const canvas = document.createElement("canvas")//, { width: 0, height: 0}) + canvas.width=canvas.height=2 // Doesn't work when 1 (?!) + const ctx = canvas.getContext('2d'); + ctx?.fillRect(0, 0, canvas.width, canvas.height); + requestAnimationFrame(function draw(){ + ctx?.fillRect(0,0, canvas.width, canvas.height) + requestAnimationFrame(draw); + }); + // Also works. Probably it should be done once connected. + //setTimeout(() => { ctx?.fillRect(0,0, canvas.width, canvas.height) }, 4000) + return canvas.captureStream(60).getTracks()[0]; +} + +export default function RequestLocalStream(): Promise { + return navigator.mediaDevices.getUserMedia({ audio:true }) + .then(aStream => { + const aTrack = aStream.getAudioTracks()[0] + if (!aTrack) { throw new Error("No audio tracks provided") } + return new _LocalStream(aTrack) + }) +} + +class _LocalStream { + private mediaRequested: boolean = false + readonly stream: MediaStream + private readonly vdTrack: MediaStreamTrack + constructor(aTrack: MediaStreamTrack) { + this.vdTrack = dummyTrack() + this.stream = new MediaStream([ aTrack, this.vdTrack ]) + } + + toggleVideo(): Promise { + if (!this.mediaRequested) { + return navigator.mediaDevices.getUserMedia({video:true}) + .then(vStream => { + const vTrack = vStream.getVideoTracks()[0] + if (!vTrack) { + throw new Error("No video track provided") + } + this.stream.addTrack(vTrack) + this.stream.removeTrack(this.vdTrack) + this.mediaRequested = true + if (this.onVideoTrackCb) { + this.onVideoTrackCb(vTrack) + } + return true + }) + .catch(e => { + // TODO: log + return false + }) + } + let enabled = true + this.stream.getVideoTracks().forEach(track => { + track.enabled = enabled = enabled && !track.enabled + }) + return Promise.resolve(enabled) + } + + toggleAudio(): boolean { + let enabled = true + this.stream.getAudioTracks().forEach(track => { + track.enabled = enabled = enabled && !track.enabled + }) + return enabled + } + + private onVideoTrackCb: ((t: MediaStreamTrack) => void) | null = null + onVideoTrack(cb: (t: MediaStreamTrack) => void) { + this.onVideoTrackCb = cb + } + + stop() { + this.stream.getTracks().forEach(t => t.stop()) + } +} + +export type LocalStream = InstanceType diff --git a/tracker/tracker-assist/src/_slim.ts b/tracker/tracker-assist/src/_slim.ts new file mode 100644 index 000000000..72c52dbd3 --- /dev/null +++ b/tracker/tracker-assist/src/_slim.ts @@ -0,0 +1,8 @@ + +/** + * Hack for the peerjs compilation on angular + * About this issue: https://github.com/peers/peerjs/issues/552 + */ + +// @ts-ignore +window.parcelRequire = window.parcelRequire || undefined; diff --git a/tracker/tracker-assist/src/index.ts b/tracker/tracker-assist/src/index.ts index 89d233e70..e09bc0ab8 100644 --- a/tracker/tracker-assist/src/index.ts +++ b/tracker/tracker-assist/src/index.ts @@ -1,19 +1,21 @@ +import './_slim'; import Peer, { MediaConnection } from 'peerjs'; import type { DataConnection } from 'peerjs'; import { App, Messages } from '@openreplay/tracker'; import type Message from '@openreplay/tracker'; +import BufferingConnection from './BufferingConnection'; import Mouse from './Mouse'; import CallWindow from './CallWindow'; import ConfirmWindow from './ConfirmWindow'; - +import RequestLocalStream from './LocalStream'; export interface Options { confirmText: string, confirmStyle: Object, // Styles object + session_calling_peer_key: string, } - enum CallingState { Requesting, True, @@ -25,17 +27,31 @@ export default function(opts: Partial = {}) { { confirmText: "You have a call. Do you want to answer?", confirmStyle: {}, + session_calling_peer_key: "__openreplay_calling_peer", }, opts, ); - return function(app: App | null, appOptions: { __DISABLE_SECURE_MODE?: boolean } = {}) { + return function(app: App | null, appOptions: { __debug_log?: boolean, __DISABLE_SECURE_MODE?: boolean } = {}) { // @ts-ignore if (app === null || !navigator?.mediaDevices?.getUserMedia) { // 93.04% browsers return; } - let assistDemandedRestart = false; - let peer : Peer | null = null; + function log(...args) { + // TODO: use centralised warn/log from tracker (?) + appOptions.__debug_log && console.log("OpenReplay Assist. ", ...args) + } + function warn(...args) { + appOptions.__debug_log && console.warn("OpenReplay Assist. ", ...args) + } + + let assistDemandedRestart = false + let peer : Peer | null = null + const openDataConnections: Record = {} + + app.addCommitCallback(function(messages) { + Object.values(openDataConnections).forEach(buffConn => buffConn.send(messages)) + }) app.attachStopCallback(function() { if (assistDemandedRestart) { return; } @@ -44,163 +60,165 @@ export default function(opts: Partial = {}) { app.attachStartCallback(function() { if (assistDemandedRestart) { return; } - const peerID = `${app.projectKey}-${app.getSessionID()}` + const peerID = `${app.getProjectKey()}-${app.getSessionID()}` peer = new Peer(peerID, { // @ts-ignore host: app.getHost(), path: '/assist', port: location.protocol === 'http:' && appOptions.__DISABLE_SECURE_MODE ? 80 : 443, }); - console.log('OpenReplay tracker-assist peerID:', peerID) - peer.on('error', e => console.log("OpenReplay tracker-assist peer error: ", e.type, e)) - peer.on('connection', function(conn) { + log('Peer created: ', peer) + peer.on('error', e => warn("Peer error: ", e.type, e)) + peer.on('connection', function(conn) { window.addEventListener("beforeunload", () => conn.open && conn.send("unload")); + log('Connecting...') - console.log('OpenReplay tracker-assist: Connecting...') conn.on('open', function() { - - console.log('OpenReplay tracker-assist: connection opened.') - - // TODO: onClose - const buffer: Message[][] = []; - let buffering = false; - function sendNext() { - if (buffer.length) { - setTimeout(() => { - conn.send(buffer.shift()); - sendNext(); - }, 50); - } else { - buffering = false; - } - } - + log('Connection opened.') assistDemandedRestart = true; app.stop(); - //@ts-ignore (should update tracker dependency) - app.addCommitCallback((messages: Array): void => { - if (!conn.open) { return; } // TODO: clear commit callbacks on connection close - let i = 0; - while (i < messages.length) { - buffer.push(messages.slice(i, i+=1000)); - } - if (!buffering) { - buffering = true; - sendNext(); - } - }); - app.start().then(() => { assistDemandedRestart = false; }); + openDataConnections[conn.peer] = new BufferingConnection(conn) + conn.on('close', () => { + log("Connection close: ", conn.peer) + delete openDataConnections[conn.peer] // TODO: check if works properly + }) + app.start().then(() => { assistDemandedRestart = false }) }); }); let callingState: CallingState = CallingState.False; + peer.on('call', function(call) { + log("Call: ", call) if (!peer) { return; } - const dataConn: DataConnection | undefined = peer - .connections[call.peer].find(c => c.type === 'data'); - if (callingState !== CallingState.False || !dataConn) { + const dataConn: DataConnection | undefined = + openDataConnections[call.peer]?.conn; + if (callingState !== CallingState.False || !dataConn || !dataConn.open) { call.close(); + warn("Call closed instantly: ", callingState, dataConn, dataConn.open) return; } + function setCallingState(newState: CallingState) { + if (newState === CallingState.True) { + sessionStorage.setItem(options.session_calling_peer_key, call.peer); + } else if (newState === CallingState.False) { + sessionStorage.removeItem(options.session_calling_peer_key); + } + callingState = newState; + } + const notifyCallEnd = () => { dataConn.open && dataConn.send("call_end"); } - callingState = CallingState.Requesting; - const confirm = new ConfirmWindow(options.confirmText, options.confirmStyle); - dataConn.on('data', (data) => { // if call closed by a caller before confirm - if (data === "call_end") { - //console.log('OpenReplay tracker-assist: receiving callend onconfirm') - callingState = CallingState.False; - confirm.remove(); - } - }); - confirm.mount(); - confirm.onAnswer(agreed => { + + let confirmAnswer: Promise + const peerOnCall = sessionStorage.getItem(options.session_calling_peer_key) + if (peerOnCall === call.peer) { + confirmAnswer = Promise.resolve(true) + } else { + setCallingState(CallingState.Requesting); + const confirm = new ConfirmWindow(options.confirmText, options.confirmStyle); + confirmAnswer = confirm.mount(); + dataConn.on('data', (data) => { // if call cancelled by a caller before confirmation + if (data === "call_end") { + log("Recieved call_end during confirm window opened") + confirm.remove(); + setCallingState(CallingState.False); + } + }); + } + + confirmAnswer.then(agreed => { if (!agreed || !dataConn.open) { - call.close(); - notifyCallEnd(); - callingState = CallingState.False; - return; + !dataConn.open && warn("Call cancelled because data connection is closed.") + call.close() + notifyCallEnd() + setCallingState(CallingState.False) + return } - const mouse = new Mouse(); - let callUI; - - const onCallConnect = lStream => { - const onCallEnd = () => { - //console.log("on callend", call.open) - mouse.remove(); - callUI?.remove(); - lStream.getTracks().forEach(t => t.stop()); - callingState = CallingState.False; - } - const initiateCallEnd = () => { - //console.log("callend initiated") - call.close() - notifyCallEnd(); - onCallEnd(); - } - - call.answer(lStream); - - dataConn.on("close", onCallEnd); + const mouse = new Mouse() + let callUI = new CallWindow() + const onCallEnd = () => { + mouse.remove(); + callUI.remove(); + setCallingState(CallingState.False); + } + const initiateCallEnd = () => { + log("initiateCallEnd") + call.close() + notifyCallEnd(); + onCallEnd(); + } + RequestLocalStream().then(lStream => { + dataConn.on("close", onCallEnd); // For what case? //call.on('close', onClose); // Works from time to time (peerjs bug) - const intervalID = setInterval(() => { + const checkConnInterval = setInterval(() => { if (!dataConn.open) { initiateCallEnd(); - clearInterval(intervalID); + clearInterval(checkConnInterval); } if (!call.open) { onCallEnd(); - clearInterval(intervalID); + clearInterval(checkConnInterval); } }, 3000); - call.on('error', initiateCallEnd); - - callUI = new CallWindow(initiateCallEnd); - callUI.setLocalStream(lStream, (stream) => { - //let videoTrack = stream.getVideoTracks()[0]; - //lStream.addTrack(videoTrack); - - //call.peerConnection.addTrack(videoTrack); - - // call.peerConnection.getSenders() - // var sender = call.peerConnection.getSenders().find(function(s) { - // return s.track .kind == videoTrack.kind; - // }); - //sender.replaceTrack(videoTrack); + call.on('error', e => { + warn("Call error:", e) + initiateCallEnd() }); + call.on('stream', function(rStream) { callUI.setRemoteStream(rStream); + const onInteraction = () => { // only if hidden? + callUI.playRemote() + document.removeEventListener("click", onInteraction) + } + document.addEventListener("click", onInteraction) }); dataConn.on('data', (data: any) => { if (data === "call_end") { - //console.log('receiving callend on call') + log('Recieved call_end during call') onCallEnd(); return; } + // if (data && typeof data.video === 'boolean') { + // log('Recieved video toggle signal: ', data.video) + // callUI.toggleRemoteVideo(data.video) + // } if (data && typeof data.name === 'string') { - //console.log("name",data) + log('Recieved name: ', data.name) callUI.setAssistentName(data.name); } if (data && typeof data.x === 'number' && typeof data.y === 'number') { mouse.move(data); } }); - } - navigator.mediaDevices.getUserMedia({video:true, audio:true}) - .then(onCallConnect) - .catch(_ => { // TODO retry only if specific error - navigator.mediaDevices.getUserMedia({audio:true}) // in case there is no camera on device - .then(onCallConnect) - .catch(e => console.log("OpenReplay tracker-assist: cant reach media devices. ", e)); + lStream.onVideoTrack(vTrack => { + const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video") + if (!sender) { + warn("No video sender found") + return + } + log("sender found:", sender) + sender.replaceTrack(vTrack) + }) + + callUI.setCallEndAction(initiateCallEnd) + callUI.setLocalStream(lStream) + call.answer(lStream.stream) + setCallingState(CallingState.True) + }) + .catch(e => { + warn("Audio mediadevice request error:", e) + onCallEnd() }); - }); + }).catch(); // in case of Confirm.remove() without any confirmation }); }); } diff --git a/tracker/tracker-axios/README.md b/tracker/tracker-axios/README.md index a092c4032..068fe3190 100644 --- a/tracker/tracker-axios/README.md +++ b/tracker/tracker-axios/README.md @@ -21,22 +21,25 @@ const tracker = new Tracker({ }); tracker.start(); -tracker.use(trackerAxios()); +tracker.use(trackerAxios({ /* options here*/ })); ``` Options: ```ts { instance: AxiosInstance; // default: axios - failuresOnly: boolean; // default: true + failuresOnly: boolean; // default: false captureWhen: (AxiosRequestConfig) => boolean; // default: () => true sessionTokenHeader: string; // default: undefined + ignoreHeaders: Array | boolean, // default [ 'Cookie', 'Set-Cookie', 'Authorization' ] } ``` By default plugin connects to the static `axios` instance, but you can specify one with the `instance` option. -Set `failuresOnly` option to `false` if you want to record every single request regardless of the status code. By default only failed requests are captured, when the axios' promise is rejected. You can also [regulate](https://github.com/axios/axios#request-config) this axios behaviour with the `validateStatus` option. +Set `failuresOnly` option to `true` if you want to record only failed requests, when the axios' promise is rejected. You can also [regulate](https://github.com/axios/axios#request-config) axios failing behaviour with the `validateStatus` option. `captureWhen` parameter allows you to set a filter on what should be captured. The function will be called with the axios config object and expected to return `true` or `false`. In case you use [OpenReplay integrations (sentry, bugsnag or others)](https://docs.openreplay.com/integrations), you can use `sessionTokenHeader` option to specify the header name. This header will be appended automatically to the each axios request and will contain OpenReplay session identificator value. + +You can define list of headers that you don't want to capture with the `ignoreHeaders` options. Set its value to `false` if you want to catch them all (`true` if opposite). By default plugin ignores the list of headers that might be sensetive such as `[ 'Cookie', 'Set-Cookie', 'Authorization' ]`. diff --git a/tracker/tracker-axios/package-lock.json b/tracker/tracker-axios/package-lock.json index 5fe0b3a1f..244d16daf 100644 --- a/tracker/tracker-axios/package-lock.json +++ b/tracker/tracker-axios/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openreplay/tracker-axios", - "version": "3.0.0", + "version": "3.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -99,12 +99,12 @@ "dev": true }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "dev": true, "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "braces": { @@ -267,9 +267,9 @@ } }, "follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", "dev": true }, "function-bind": { @@ -761,9 +761,9 @@ } }, "trim-newlines": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", - "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, "type-fest": { diff --git a/tracker/tracker-axios/package.json b/tracker/tracker-axios/package.json index f013633ef..0d6de4ca1 100644 --- a/tracker/tracker-axios/package.json +++ b/tracker/tracker-axios/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker-axios", "description": "Tracker plugin for axios requests recording", - "version": "3.0.1", + "version": "3.4.1", "keywords": [ "axios", "logging", @@ -21,11 +21,11 @@ "dependencies": {}, "peerDependencies": { "@openreplay/tracker": "^3.0.0", - "axios": "^0.21.1" + "axios": "^0.21.2" }, "devDependencies": { "@openreplay/tracker": "^3.0.0", - "axios": "^0.21.1", + "axios": "^0.21.2", "prettier": "^1.18.2", "replace-in-files-cli": "^1.0.0", "typescript": "^3.6.4" diff --git a/tracker/tracker-axios/src/index.ts b/tracker/tracker-axios/src/index.ts index 3c28260ca..01eb79dca 100644 --- a/tracker/tracker-axios/src/index.ts +++ b/tracker/tracker-axios/src/index.ts @@ -9,16 +9,16 @@ export interface Options { instance: AxiosInstance; failuresOnly: boolean; captureWhen: (AxiosRequestConfig) => boolean; - //ingoreHeaders: Array | boolean; + ignoreHeaders: Array | boolean; } export default function(opts: Partial = {}) { const options: Options = Object.assign( { instance: axios, - failuresOnly: true, + failuresOnly: false, captureWhen: () => true, - //ingoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ], + ignoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ], }, opts, ); @@ -27,48 +27,80 @@ export default function(opts: Partial = {}) { return; } - const sendFetchMessage = (response: AxiosResponse) => { + const ihOpt = options.ignoreHeaders + const isHIgnoring = Array.isArray(ihOpt) + ? name => ihOpt.includes(name) + : () => ihOpt + + const sendFetchMessage = (res: AxiosResponse) => { // @ts-ignore - const startTime: number = response.config.__openreplayStartTs; + const startTime: number = res.config.__openreplayStartTs; const duration = performance.now() - startTime; if (typeof startTime !== 'number') { return; } - let requestData: string = ''; - if (typeof response.config.data === 'string') { - requestData = response.config.data; + let reqBody: string = ''; + if (typeof res.config.data === 'string') { + reqBody = res.config.data; } else { try { - requestData = JSON.stringify(response.config.data) || ''; - } catch (e) {} + reqBody = JSON.stringify(res.config.data) || ''; + } catch (e) {} // TODO: app debug } - let responseData: string = ''; - if (typeof response.data === 'string') { - responseData = response.data; + let resBody: string = ''; + if (typeof res.data === 'string') { + resBody = res.data; } else { try { - responseData = JSON.stringify(response.data) || ''; + resBody = JSON.stringify(res.data) || ''; } catch (e) {} } + const reqHs: Record = {} + const resHs: Record = {} + // TODO: type safe axios headers + if (ihOpt !== true) { + function writeReqHeader([n, v]: [string, string]) { + if (!isHIgnoring(n)) { reqHs[n] = v } + } + if (res.config.headers instanceof Headers) { + res.config.headers.forEach((v, n) => writeReqHeader([n, v])) + } else if (Array.isArray(res.config.headers)) { + res.config.headers.forEach(writeReqHeader); + } else if (typeof res.config.headers === 'object') { + Object.entries(res.config.headers as Record).forEach(writeReqHeader) + } + + // TODO: type safe axios headers + if (typeof res.headers === 'object') { + Object.entries(res.headers as Record).forEach(([v, n]) => { if (!isHIgnoring(n)) resHs[n] = v }) + } + } + // Why can't axios propogate the final request URL somewhere? - const fullURL = buildFullPath(response.config.baseURL, options.instance.getUri(response.config)); + const fullURL = buildFullPath(res.config.baseURL, options.instance.getUri(res.config)); app.send( Messages.Fetch( - typeof response.config.method === 'string' ? response.config.method.toUpperCase() : 'GET', + typeof res.config.method === 'string' ? res.config.method.toUpperCase() : 'GET', fullURL, - requestData, - responseData, - response.status, + JSON.stringify({ + headers: reqHs, + body: reqBody, + }), + JSON.stringify({ + headers: resHs, + body: resBody, + }), + res.status, startTime + performance.timing.navigationStart, duration, ), ); } - + // TODO: why app.safe doesn't work here? options.instance.interceptors.request.use(function (config) { if (options.sessionTokenHeader) { const sessionToken = app.getSessionToken(); @@ -80,7 +112,7 @@ export default function(opts: Partial = {}) { config.headers.append(options.sessionTokenHeader, sessionToken); } else if (Array.isArray(config.headers)) { config.headers.push([options.sessionTokenHeader, sessionToken]); - } else { + } else if (typeof config.headers === 'object') { config.headers[options.sessionTokenHeader] = sessionToken; } } diff --git a/tracker/tracker-axios/tsconfig.json b/tracker/tracker-axios/tsconfig.json index ce07a685b..dd1ee258f 100644 --- a/tracker/tracker-axios/tsconfig.json +++ b/tracker/tracker-axios/tsconfig.json @@ -7,6 +7,7 @@ "module": "es6", "moduleResolution": "node", "declaration": true, - "outDir": "./lib" + "outDir": "./lib", + "lib": ["es6", "dom", "es2017"] // is all necessary? } } diff --git a/tracker/tracker-fetch/README.md b/tracker/tracker-fetch/README.md index d72201e35..b7fca2e4b 100644 --- a/tracker/tracker-fetch/README.md +++ b/tracker/tracker-fetch/README.md @@ -1,7 +1,7 @@ # OpenReplay Tracker Fetch plugin Tracker plugin to support tracking of the `fetch` requests payload. -Additionally it populates the requests with `sessionID` header for backend logging. +Additionally it populates the requests with `sessionToken` header for backend logging. ## Installation @@ -23,13 +23,24 @@ const tracker = new Tracker({ }); tracker.start(); -export const fetch = tracker.use(trackerFetch({ - sessionTokenHeader: 'X-Session-ID', // optional - failuresOnly: true //optional -})); +export const fetch = tracker.use(trackerFetch({ /* options here*/ })); fetch('https://my.api.io/resource').then(response => response.json()).then(body => console.log(body)); ``` -In case you use OpenReplay integrations (sentry, bugsnag or others), you can use `sessionTokenHeader` option to specify the header name. This header will be appended automatically to the each fetch request and will contain OpenReplay session identificator value. -Set `failuresOnly` option to `true` if you want to record only requests with the status code >= 400. \ No newline at end of file +Options: +```ts +{ + failuresOnly: boolean, // default false + sessionTokenHeader: string | undefined, // default undefined + ignoreHeaders: Array | boolean, // default [ 'Cookie', 'Set-Cookie', 'Authorization' ] +} + +``` + +Set `failuresOnly` option to `true` if you want to record only requests with the status code >= 400. + +In case you use [OpenReplay integrations (sentry, bugsnag or others)](https://docs.openreplay.com/integrations), you can use `sessionTokenHeader` option to specify the header name. This header will be appended automatically to the each fetch request and will contain OpenReplay session identificator value. + +You can define list of headers that you don't want to capture with the `ignoreHeaders` options. Set its value to `false` if you want to catch them all (`true` if opposite). By default plugin ignores the list of headers that might be sensetive such as `[ 'Cookie', 'Set-Cookie', 'Authorization' ]`. + diff --git a/tracker/tracker-fetch/package-lock.json b/tracker/tracker-fetch/package-lock.json index b8d27b87e..16afd2f75 100644 --- a/tracker/tracker-fetch/package-lock.json +++ b/tracker/tracker-fetch/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openreplay/tracker-fetch", - "version": "3.0.0", + "version": "3.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tracker/tracker-fetch/package.json b/tracker/tracker-fetch/package.json index 237a6e326..1d650bf6e 100644 --- a/tracker/tracker-fetch/package.json +++ b/tracker/tracker-fetch/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker-fetch", "description": "Tracker plugin for fetch requests recording ", - "version": "3.0.0", + "version": "3.4.1", "keywords": [ "fetch", "logging", diff --git a/tracker/tracker-fetch/src/index.ts b/tracker/tracker-fetch/src/index.ts index 3bfa30034..fbce7ac31 100644 --- a/tracker/tracker-fetch/src/index.ts +++ b/tracker/tracker-fetch/src/index.ts @@ -2,13 +2,17 @@ import { App, Messages } from '@openreplay/tracker'; export interface Options { sessionTokenHeader?: string; - failuresOnly?: boolean; + replaceDefault: boolean; // overrideDefault ? + failuresOnly: boolean; + ignoreHeaders: Array | boolean; } export default function(opts: Partial = {}) { const options: Options = Object.assign( { + replaceDefault: false, failuresOnly: false, + ignoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ], }, opts, ); @@ -18,7 +22,12 @@ export default function(opts: Partial = {}) { return window.fetch; } - return async (input: RequestInfo, init: RequestInit = {}) => { + const ihOpt = options.ignoreHeaders + const isHIgnoring = Array.isArray(ihOpt) + ? name => ihOpt.includes(name) + : () => ihOpt + + const fetch = async (input: RequestInfo, init: RequestInit = {}) => { if (typeof input !== 'string') { return window.fetch(input, init); } @@ -44,20 +53,50 @@ export default function(opts: Partial = {}) { return response } const r = response.clone(); - r.text().then(text => + + r.text().then(text => { + const reqHs: Record = {} + const resHs: Record = {} + if (ihOpt !== true) { + function writeReqHeader([n, v]) { + if (!isHIgnoring(n)) { reqHs[n] = v } + } + if (init.headers instanceof Headers) { + init.headers.forEach((v, n) => writeReqHeader([n, v])) + } else if (Array.isArray(init.headers)) { + init.headers.forEach(writeReqHeader); + } else if (typeof init.headers === 'object') { + Object.entries(init.headers).forEach(writeReqHeader) + } + + r.headers.forEach((v, n) => { if (!isHIgnoring(n)) resHs[n] = v }) + } + const req = JSON.stringify({ + headers: reqHs, + body: typeof init.body === 'string' ? init.body : '', + }) + const res = JSON.stringify({ + headers: resHs, + body: text, + }) app.send( Messages.Fetch( - typeof init.method === 'string' ? init.method : 'GET', + typeof init.method === 'string' ? init.method.toUpperCase() : 'GET', input, - typeof init.body === 'string' ? init.body : '', - text, + req, + res, r.status, startTime + performance.timing.navigationStart, duration, ), - ), - ); + ) + }); return response; }; + if (options.replaceDefault) { + window.fetch = fetch + } + return fetch; }; + } diff --git a/tracker/tracker-fetch/tsconfig.json b/tracker/tracker-fetch/tsconfig.json index ce07a685b..258c2f510 100644 --- a/tracker/tracker-fetch/tsconfig.json +++ b/tracker/tracker-fetch/tsconfig.json @@ -7,6 +7,7 @@ "module": "es6", "moduleResolution": "node", "declaration": true, - "outDir": "./lib" + "outDir": "./lib", + "lib": ["es6", "dom", "es2017"] } } diff --git a/tracker/tracker-mobx/package-lock.json b/tracker/tracker-mobx/package-lock.json index f6acb3063..7e115ddcd 100644 --- a/tracker/tracker-mobx/package-lock.json +++ b/tracker/tracker-mobx/package-lock.json @@ -264,9 +264,9 @@ "dev": true }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -545,9 +545,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-type": { @@ -752,9 +752,9 @@ } }, "trim-newlines": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", - "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, "type-fest": { diff --git a/tracker/tracker/package-lock.json b/tracker/tracker/package-lock.json index 2d6f47ed9..8d1c160b5 100644 --- a/tracker/tracker/package-lock.json +++ b/tracker/tracker/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openreplay/tracker", - "version": "3.2.5", + "version": "3.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index c6049da67..41934717c 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.3.0", + "version": "3.4.4", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 8102df880..b50bb5731 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -1,5 +1,5 @@ -import { timestamp, log } from '../utils'; -import { Timestamp, TechnicalInfo, PageClose } from '../../messages'; +import { timestamp, log, warn } from '../utils'; +import { Timestamp, PageClose } from '../../messages'; import Message from '../../messages/message'; import Nodes from './nodes'; import Observer from './observer'; @@ -24,8 +24,11 @@ export type Options = { session_pageno_key: string; local_uuid_key: string; ingestPoint: string; + resourceBaseHref: string | null, // resourceHref? + //resourceURLRewriter: (url: string) => string | boolean, __is_snippet: boolean; __debug_report_edp: string | null; + __debug_log: boolean; onStart?: (info: OnStartInfo) => void; } & ObserverOptions & WebworkerOptions; @@ -65,10 +68,13 @@ export default class App { session_pageno_key: '__openreplay_pageno', local_uuid_key: '__openreplay_uuid', ingestPoint: DEFAULT_INGEST_POINT, + resourceBaseHref: null, __is_snippet: false, __debug_report_edp: null, + __debug_log: false, obscureTextEmails: true, obscureTextNumbers: false, + captureIFrames: false, }, opts, ); @@ -86,10 +92,9 @@ export default class App { new Blob([`WEBWORKER_BODY`], { type: 'text/javascript' }), ), ); - // this.worker.onerror = e => { - // this.send(new TechnicalInfo("webworker_error", JSON.stringify(e))); - // /* TODO: send report */ - // } + this.worker.onerror = e => { + this._debug("webworker_error", e) + } let lastTs = timestamp(); let fileno = 0; this.worker.onmessage = ({ data }: MessageEvent) => { @@ -110,20 +115,24 @@ export default class App { this.attachEventListener(document, 'mouseleave', alertWorker, false, false); this.attachEventListener(document, 'visibilitychange', alertWorker, false); } catch (e) { - this.sendDebugReport("worker_start", e); + this._debug("worker_start", e); } } - private sendDebugReport(context: string, e: any) { + private _debug(context: string, e: any) { if(this.options.__debug_report_edp !== null) { fetch(this.options.__debug_report_edp, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, error: `${e}` }) }); } + if(this.options.__debug_log) { + warn("OpenReplay errror: ", context, e) + } } send(message: Message, urgent = false): void { @@ -155,12 +164,11 @@ export default class App { try { fn.apply(this, args); } catch (e) { - app.send(new TechnicalInfo("error", JSON.stringify({ - time: timestamp(), - name: e.name, - message: e.message, - stack: e.stack - }))); + app._debug("safe_fn_call", e) + // time: timestamp(), + // name: e.name, + // message: e.message, + // stack: e.stack } } as any // TODO: correct typing } @@ -199,11 +207,34 @@ export default class App { return this._sessionID || undefined; } getHost(): string { - return new URL(this.options.ingestPoint).host; + return new URL(this.options.ingestPoint).hostname + } + getProjectKey(): string { + return this.projectKey + } + getBaseHref(): string { + if (typeof this.options.resourceBaseHref === 'string') { + return this.options.resourceBaseHref + } else if (typeof this.options.resourceBaseHref === 'object') { + //switch between types + } + if (document.baseURI) { + return document.baseURI + } + // IE only + return document.head + ?.getElementsByTagName("base")[0] + ?.getAttribute("href") || location.origin + location.pathname + } + resolveResourceURL(resourceURL: string): string { + const base = new URL(this.getBaseHref()) + base.pathname += "/" + new URL(resourceURL).pathname + base.pathname.replace(/\/+/g, "/") + return base.toString() } isServiceURL(url: string): boolean { - return url.startsWith(this.options.ingestPoint); + return url.startsWith(this.options.ingestPoint) } active(): boolean { @@ -211,10 +242,10 @@ export default class App { } private _start(reset: boolean): Promise { if (!this.isActive) { - this.isActive = true; if (!this.worker) { - throw new Error("Stranger things: no worker found"); + return Promise.reject("No worker found: perhaps, CSP is not set."); } + this.isActive = true; let pageNo: number = 0; const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); @@ -273,7 +304,7 @@ export default class App { this._sessionID = sessionID; } if (!this.worker) { - throw new Error("Stranger things: no worker found after start request"); + throw new Error("no worker found after start request (this might not happen)"); } this.worker.postMessage({ token, beaconSizeLimit }); this.startCallbacks.forEach((cb) => cb()); @@ -289,7 +320,8 @@ export default class App { }) .catch(e => { this.stop(); - this.sendDebugReport("session_start", e); + warn("OpenReplay was unable to start. ", e) + this._debug("session_start", e); throw e; }) } diff --git a/tracker/tracker/src/main/app/observer.ts b/tracker/tracker/src/main/app/observer.ts index 88168cad8..493c7aaac 100644 --- a/tracker/tracker/src/main/app/observer.ts +++ b/tracker/tracker/src/main/app/observer.ts @@ -1,4 +1,4 @@ -import { stars, hasOpenreplayAttribute, getBaseURI } from '../utils'; +import { stars, hasOpenreplayAttribute } from '../utils'; import { CreateDocument, CreateElementNode, @@ -10,37 +10,49 @@ import { RemoveNodeAttribute, MoveNode, RemoveNode, + CreateIFrameDocument, } from '../../messages'; import App from './index'; +interface Window extends WindowProxy { + HTMLInputElement: typeof HTMLInputElement, + HTMLLinkElement: typeof HTMLLinkElement, + HTMLStyleElement: typeof HTMLStyleElement, + SVGStyleElement: typeof SVGStyleElement, + HTMLIFrameElement: typeof HTMLIFrameElement, + Text: typeof Text, + Element: typeof Element, + //parent: Window, +} + + +type WindowConstructor = + Document | + Element | + Text | + HTMLInputElement | + HTMLLinkElement | + HTMLStyleElement | + HTMLIFrameElement + +// type ConstructorNames = +// 'Element' | +// 'Text' | +// 'HTMLInputElement' | +// 'HTMLLinkElement' | +// 'HTMLStyleElement' | +// 'HTMLIFrameElement' +type Constructor = { new (...args: any[]): T , name: string }; + + function isSVGElement(node: Element): node is SVGElement { return node.namespaceURI === 'http://www.w3.org/2000/svg'; } -function isIgnored(node: Node): boolean { - if (node instanceof Text) { - return false; - } - if (!(node instanceof Element)) { - return true; - } - const tag = node.tagName.toUpperCase(); - if (tag === 'LINK') { - const rel = node.getAttribute('rel'); - const as = node.getAttribute('as'); - return !(rel?.includes('stylesheet') || as === "style" || as === "font"); - } - return ( - tag === 'SCRIPT' || - tag === 'NOSCRIPT' || - tag === 'META' || - tag === 'TITLE' || - tag === 'BASE' - ); -} export interface Options { obscureTextEmails: boolean; obscureTextNumbers: boolean; + captureIFrames: boolean; } export default class Observer { @@ -51,17 +63,33 @@ export default class Observer { private readonly attributesList: Array | undefined>; private readonly textSet: Set; private readonly textMasked: Set; - private readonly options: Options; - constructor(private readonly app: App, opts: Options) { - this.options = opts; + constructor(private readonly app: App, private readonly options: Options, private readonly context: Window = window) { this.observer = new MutationObserver( this.app.safe((mutations) => { for (const mutation of mutations) { const target = mutation.target; - if (isIgnored(target) || !document.contains(target)) { + const type = mutation.type; + + // Special case + // Document 'childList' might happen in case of iframe. + // TODO: generalize as much as possible + if (this.isInstance(target, Document) + && type === 'childList' + //&& new Array(mutation.addedNodes).some(node => this.isInstance(node, HTMLHtmlElement)) + ) { + const parentFrame = target.defaultView?.frameElement + if (!parentFrame) { continue } + this.bindTree(target.documentElement) + const frameID = this.app.nodes.getID(parentFrame) + const docID = this.app.nodes.getID(target.documentElement) + if (frameID === undefined || docID === undefined) { continue } + this.app.send(CreateIFrameDocument(frameID, docID)); + continue; + } + + if (this.isIgnored(target) || !context.document.contains(target)) { continue; } - const type = mutation.type; if (type === 'childList') { for (let i = 0; i < mutation.removedNodes.length; i++) { this.bindTree(mutation.removedNodes[i]); @@ -114,6 +142,43 @@ export default class Observer { this.textMasked.clear(); } + // TODO: we need a type expert here so we won't have to ignore the lines + private isInstance(node: Node, constr: Constructor): node is T { + let context = this.context; + while(context.parent && context.parent !== context) { + // @ts-ignore + if (node instanceof context[constr.name]) { + return true + } + // @ts-ignore + context = context.parent + } + // @ts-ignore + return node instanceof context[constr.name] + } + + private isIgnored(node: Node): boolean { + if (this.isInstance(node, Text)) { + return false; + } + if (!this.isInstance(node, Element)) { + return true; + } + const tag = node.tagName.toUpperCase(); + if (tag === 'LINK') { + const rel = node.getAttribute('rel'); + const as = node.getAttribute('as'); + return !(rel?.includes('stylesheet') || as === "style" || as === "font"); + } + return ( + tag === 'SCRIPT' || + tag === 'NOSCRIPT' || + tag === 'META' || + tag === 'TITLE' || + tag === 'BASE' + ); + } + private sendNodeAttribute( id: number, node: Element, @@ -130,7 +195,7 @@ export default class Observer { if (value.length > 1e5) { value = ''; } - this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI())); + this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); } else { this.app.send(new SetNodeAttribute(id, name, value)); } @@ -148,7 +213,7 @@ export default class Observer { } if ( name === 'value' && - node instanceof HTMLInputElement && + this.isInstance(node, HTMLInputElement) && node.type !== 'button' && node.type !== 'reset' && node.type !== 'submit' @@ -159,8 +224,8 @@ export default class Observer { this.app.send(new RemoveNodeAttribute(id, name)); return; } - if (name === 'style' || name === 'href' && node instanceof HTMLLinkElement) { - this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI())); + if (name === 'style' || name === 'href' && this.isInstance(node, HTMLLinkElement)) { + this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref())); return; } if (name === 'href' || value.length > 1e5) { @@ -170,8 +235,8 @@ export default class Observer { } private sendNodeData(id: number, parentElement: Element, data: string): void { - if (parentElement instanceof HTMLStyleElement || parentElement instanceof SVGStyleElement) { - this.app.send(new SetCSSDataURLBased(id, data, getBaseURI())); + if (this.isInstance(parentElement, HTMLStyleElement) || this.isInstance(parentElement, SVGStyleElement)) { + this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref())); return; } if (this.textMasked.has(id)) { @@ -201,7 +266,7 @@ export default class Observer { } private bindTree(node: Node): void { - if (isIgnored(node)) { + if (this.isIgnored(node)) { return; } this.bindNode(node); @@ -210,7 +275,7 @@ export default class Observer { NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, { acceptNode: (node) => - isIgnored(node) || this.app.nodes.getID(node) !== undefined + this.isIgnored(node) || this.app.nodes.getID(node) !== undefined ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, }, @@ -231,7 +296,9 @@ export default class Observer { private _commitNode(id: number, node: Node): boolean { const parent = node.parentNode; let parentID: number | undefined; - if (id !== 0) { + if (this.isInstance(node, HTMLHtmlElement)) { + this.indexes[id] = 0 + } else { if (parent === null) { this.unbindNode(node); return false; @@ -247,7 +314,7 @@ export default class Observer { } if ( this.textMasked.has(parentID) || - (node instanceof Element && hasOpenreplayAttribute(node, 'masked')) + (this.isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked')) ) { this.textMasked.add(id); } @@ -271,7 +338,7 @@ export default class Observer { throw 'commitNode: missing node index'; } if (isNew === true) { - if (node instanceof Element) { + if (this.isInstance(node, Element)) { if (parentID !== undefined) { this.app.send(new CreateElementNode( @@ -287,7 +354,12 @@ export default class Observer { const attr = node.attributes[i]; this.sendNodeAttribute(id, node, attr.nodeName, attr.value); } - } else if (node instanceof Text) { + + if (this.isInstance(node, HTMLIFrameElement) && + (this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) { + this.handleIframe(node); + } + } else if (this.isInstance(node, Text)) { // for text node id != 0, hence parentID !== undefined and parent is Element this.app.send(new CreateTextNode(id, parentID as number, index)); this.sendNodeData(id, parent as Element, node.data); @@ -299,7 +371,7 @@ export default class Observer { } const attr = this.attributesList[id]; if (attr !== undefined) { - if (!(node instanceof Element)) { + if (!this.isInstance(node, Element)) { throw 'commitNode: node is not an element'; } for (const name of attr) { @@ -307,7 +379,7 @@ export default class Observer { } } if (this.textSet.has(id)) { - if (!(node instanceof Text)) { + if (!this.isInstance(node, Text)) { throw 'commitNode: node is not a text'; } // for text node id != 0, hence parent is Element @@ -337,8 +409,44 @@ export default class Observer { this.clear(); } + private iframeObservers: Observer[] = []; + private handleIframe(iframe: HTMLIFrameElement): void { + const handle = () => { + const context = iframe.contentWindow as Window | null + const id = this.app.nodes.getID(iframe) + if (!context || id === undefined) { return } + + const observer = new Observer(this.app, this.options, context) + this.iframeObservers.push(observer) + observer.observeIframe(id, context) + } + this.app.attachEventListener(iframe, "load", handle) + handle() + } + + // TODO: abstract common functionality, separate FrameObserver + private observeIframe(id: number, context: Window) { + const doc = context.document; + this.observer.observe(doc, { + childList: true, + attributes: true, + characterData: true, + subtree: true, + attributeOldValue: false, + characterDataOldValue: false, + }); + this.bindTree(doc.documentElement); + const docID = this.app.nodes.getID(doc.documentElement); + if (docID === undefined) { + console.log("Wrong") + return; + } + this.app.send(CreateIFrameDocument(id,docID)); + this.commitNodes(); + } + observe(): void { - this.observer.observe(document, { + this.observer.observe(this.context.document, { childList: true, attributes: true, characterData: true, @@ -347,11 +455,13 @@ export default class Observer { characterDataOldValue: false, }); this.app.send(new CreateDocument()); - this.bindTree(document.documentElement); + this.bindTree(this.context.document.documentElement); this.commitNodes(); } disconnect(): void { + this.iframeObservers.forEach(o => o.disconnect()); + this.iframeObservers = []; this.observer.disconnect(); this.clear(); } diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index 79e2cbee8..ca0dd9208 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -17,7 +17,7 @@ import Scroll from './modules/scroll'; import Viewport from './modules/viewport'; import Longtasks from './modules/longtasks'; import CSSRules from './modules/cssrules'; -import { IN_BROWSER, deprecationWarn } from './utils'; +import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils'; import { Options as AppOptions } from './app'; import { Options as ConsoleOptions } from './modules/console'; @@ -41,13 +41,13 @@ const DOCS_SETUP = '/installation/setup-or'; function processOptions(obj: any): obj is Options { if (obj == null) { - console.error(`OpenReplay: invalid options argument type. Please, check documentation on https://docs.openreplay.com${ DOCS_SETUP }`); + console.error(`OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`); return false; } if (typeof obj.projectKey !== 'string') { if (typeof obj.projectKey !== 'number') { if (typeof obj.projectID !== 'number') { // Back compatability - console.error(`OpenReplay: projectKey is missing or wrong type (string is expected). Please, check https://docs.openreplay.com${ DOCS_SETUP } for more information.`) + console.error(`OpenReplay: projectKey is missing or wrong type (string is expected). Please, check ${DOCS_HOST}${DOCS_SETUP} for more information.`) return false } else { obj.projectKey = obj.projectID.toString(); @@ -59,7 +59,7 @@ function processOptions(obj: any): obj is Options { } } if (typeof obj.sessionToken !== 'string' && obj.sessionToken != null) { - console.warn(`OpenReplay: invalid options argument type. Please, check documentation on https://docs.openreplay.com${ DOCS_SETUP }`) + console.warn(`OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`) } return true; } @@ -70,6 +70,10 @@ export default class API { if (!IN_BROWSER || !processOptions(options)) { return; } + if ((window as any).__OPENREPLAY__) { + console.error("OpenReplay: one tracker instance has been initialised already") + return + } if (!options.__DISABLE_SECURE_MODE && location.protocol !== 'https:') { console.error("OpenReplay: Your website must be publicly accessible and running on SSL in order for OpenReplay to properly capture and replay the user session. You can disable this check by setting `__DISABLE_SECURE_MODE` option to `true` if you are testing in localhost. Keep in mind, that asset files on a local machine are not available to the outside world. This might affect tracking if you use css files.") return; @@ -99,9 +103,9 @@ export default class API { Performance(this.app, options); Scroll(this.app); Longtasks(this.app); - (window as any).__OPENREPLAY__ = (window as any).__OPENREPLAY__ || this; + (window as any).__OPENREPLAY__ = this; } else { - console.log("OpenReplay: browser doesn't support API required for tracking.") + console.log("OpenReplay: browser doesn't support API required for tracking or doNotTrack is set to 1.") const req = new XMLHttpRequest(); const orig = options.ingestPoint || DEFAULT_INGEST_POINT; req.open("POST", orig + "/v1/web/not-started"); @@ -133,7 +137,7 @@ export default class API { start(): void { if (!IN_BROWSER) { - console.error(`OpenReplay: you are trying to start Tracker on a node.js environment. If you want to use OpenReplay with SSR, please, use componentDidMount or useEffect API for placing the \`tracker.start()\` line. Check documentation on https://docs.openreplay.com${ DOCS_SETUP }`) + console.error(`OpenReplay: you are trying to start Tracker on a node.js environment. If you want to use OpenReplay with SSR, please, use componentDidMount or useEffect API for placing the \`tracker.start()\` line. Check documentation on ${DOCS_HOST}${DOCS_SETUP}`) return; } if (this.app === null) { diff --git a/tracker/tracker/src/main/modules/console.ts b/tracker/tracker/src/main/modules/console.ts index 251ff8ca1..34be0264a 100644 --- a/tracker/tracker/src/main/modules/console.ts +++ b/tracker/tracker/src/main/modules/console.ts @@ -123,7 +123,7 @@ export default function (app: App, opts: Partial): void { options.consoleMethods.forEach((method) => { if (consoleMethods.indexOf(method) === -1) { - console.error(`Asayer: unsupported console method ${method}`); + console.error(`OpenReplay: unsupported console method "${method}"`); return; } const fn = (console as any)[method]; diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 366a7d3fe..54166f717 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,6 +1,5 @@ import App from '../app'; import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../messages'; -import { getBaseURI } from '../utils'; export default function(app: App | null) { if (app === null) { @@ -14,7 +13,7 @@ export default function(app: App | null) { const processOperation = app.safe( (stylesheet: CSSStyleSheet, index: number, rule?: string) => { const sendMessage = typeof rule === 'string' - ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, getBaseURI())) + ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref())) : (nodeID: number) => app.send(new CSSDeleteRule(nodeID, index)); // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule) if (stylesheet.ownerNode == null) { diff --git a/tracker/tracker/src/main/modules/img.ts b/tracker/tracker/src/main/modules/img.ts index d7d4a6be5..e20a4d531 100644 --- a/tracker/tracker/src/main/modules/img.ts +++ b/tracker/tracker/src/main/modules/img.ts @@ -1,4 +1,4 @@ -import { timestamp, isURL, getBaseURI } from '../utils'; +import { timestamp, isURL } from '../utils'; import App from '../app'; import { ResourceTiming, SetNodeAttributeURLBased } from '../../messages'; @@ -17,7 +17,7 @@ export default function (app: App): void { app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, src, 'img')); } } else if (src.length < 1e5) { - app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI())); + app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); } }); @@ -30,7 +30,7 @@ export default function (app: App): void { return; } const src = target.src; - app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI())); + app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref())); } } }); diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts index 96b973e14..40bcbeb61 100644 --- a/tracker/tracker/src/main/modules/mouse.ts +++ b/tracker/tracker/src/main/modules/mouse.ts @@ -72,14 +72,23 @@ function getTargetLabel(target: Element): string { return ''; } +interface HeatmapsOptions { + finder: FinderOptions, +} + export interface Options { - selectorFinder: boolean | FinderOptions; + heatmaps: boolean | HeatmapsOptions; } export default function (app: App, opts: Partial): void { const options: Options = Object.assign( { - selectorFinder: true, + heatmaps: false // { + // finder: { + // threshold: 5, + // maxNumberOfTries: 600, + // }, + // }, }, opts, ); @@ -106,9 +115,9 @@ export default function (app: App, opts: Partial): void { const selectorMap: {[id:number]: string} = {}; function getSelector(id: number, target: Element): string { - if (options.selectorFinder === false) { return '' } + if (options.heatmaps === false) { return '' } return selectorMap[id] = selectorMap[id] || - finder(target, options.selectorFinder === true ? undefined : options.selectorFinder); + finder(target, options.heatmaps === true ? undefined : options.heatmaps.finder); } app.attachEventListener( diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 350447b62..5a8700f31 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -16,22 +16,13 @@ export function isURL(s: string): boolean { return s.substr(0, 8) === 'https://' || s.substr(0, 7) === 'http://'; } -export function getBaseURI(): string { - if (document.baseURI) { - return document.baseURI; - } - // IE only - return document.head - ?.getElementsByTagName("base")[0] - ?.getAttribute("href") || location.origin + location.pathname; -} - export const IN_BROWSER = !(typeof window === "undefined"); export const log = console.log export const warn = console.warn -const DOCS_HOST = 'https://docs.openreplay.com'; +export const DOCS_HOST = 'https://docs.openreplay.com'; + const warnedFeatures: { [key: string]: boolean; } = {}; export function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath: string = "/"): void { if (warnedFeatures[ nameOfFeature ]) { @@ -66,3 +57,4 @@ export function hasOpenreplayAttribute(e: Element, name: string): boolean { return false; } + diff --git a/tracker/tracker/src/main/vendors/finder/finder.d.ts b/tracker/tracker/src/main/vendors/finder/finder.d.ts deleted file mode 100644 index aaff849fb..000000000 --- a/tracker/tracker/src/main/vendors/finder/finder.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export declare type Options = { - root: Element; - idName: (name: string) => boolean; - className: (name: string) => boolean; - tagName: (name: string) => boolean; - attr: (name: string, value: string) => boolean; - seedMinLength: number; - optimizedMinLength: number; - threshold: number; - maxNumberOfTries: number; -}; -export declare function finder(input: Element, options?: Partial): string; diff --git a/tracker/tracker/src/main/vendors/finder/finder.js b/tracker/tracker/src/main/vendors/finder/finder.js deleted file mode 100644 index 0e5eab2d7..000000000 --- a/tracker/tracker/src/main/vendors/finder/finder.js +++ /dev/null @@ -1,339 +0,0 @@ -var Limit; -(function (Limit) { - Limit[Limit["All"] = 0] = "All"; - Limit[Limit["Two"] = 1] = "Two"; - Limit[Limit["One"] = 2] = "One"; -})(Limit || (Limit = {})); -let config; -let rootDocument; -export function finder(input, options) { - if (input.nodeType !== Node.ELEMENT_NODE) { - throw new Error(`Can't generate CSS selector for non-element node type.`); - } - if ("html" === input.tagName.toLowerCase()) { - return "html"; - } - const defaults = { - root: document.body, - idName: (name) => true, - className: (name) => true, - tagName: (name) => true, - attr: (name, value) => false, - seedMinLength: 1, - optimizedMinLength: 2, - threshold: 1000, - maxNumberOfTries: 10000, - }; - config = Object.assign(Object.assign({}, defaults), options); - rootDocument = findRootDocument(config.root, defaults); - let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One))); - if (path) { - const optimized = sort(optimize(path, input)); - if (optimized.length > 0) { - path = optimized[0]; - } - return selector(path); - } - else { - throw new Error(`Selector was not found.`); - } -} -function findRootDocument(rootNode, defaults) { - if (rootNode.nodeType === Node.DOCUMENT_NODE) { - return rootNode; - } - if (rootNode === defaults.root) { - return rootNode.ownerDocument; - } - return rootNode; -} -function bottomUpSearch(input, limit, fallback) { - let path = null; - let stack = []; - let current = input; - let i = 0; - while (current && current !== config.root.parentElement) { - let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()]; - const nth = index(current); - if (limit === Limit.All) { - if (nth) { - level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth))); - } - } - else if (limit === Limit.Two) { - level = level.slice(0, 1); - if (nth) { - level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth))); - } - } - else if (limit === Limit.One) { - const [node] = level = level.slice(0, 1); - if (nth && dispensableNth(node)) { - level = [nthChild(node, nth)]; - } - } - for (let node of level) { - node.level = i; - } - stack.push(level); - if (stack.length >= config.seedMinLength) { - path = findUniquePath(stack, fallback); - if (path) { - break; - } - } - current = current.parentElement; - i++; - } - if (!path) { - path = findUniquePath(stack, fallback); - } - return path; -} -function findUniquePath(stack, fallback) { - const paths = sort(combinations(stack)); - if (paths.length > config.threshold) { - return fallback ? fallback() : null; - } - for (let candidate of paths) { - if (unique(candidate)) { - return candidate; - } - } - return null; -} -function selector(path) { - let node = path[0]; - let query = node.name; - for (let i = 1; i < path.length; i++) { - const level = path[i].level || 0; - if (node.level === level - 1) { - query = `${path[i].name} > ${query}`; - } - else { - query = `${path[i].name} ${query}`; - } - node = path[i]; - } - return query; -} -function penalty(path) { - return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0); -} -function unique(path) { - switch (rootDocument.querySelectorAll(selector(path)).length) { - case 0: - throw new Error(`Can't select any node with this selector: ${selector(path)}`); - case 1: - return true; - default: - return false; - } -} -function id(input) { - const elementId = input.getAttribute("id"); - if (elementId && config.idName(elementId)) { - return { - name: "#" + cssesc(elementId, { isIdentifier: true }), - penalty: 0, - }; - } - return null; -} -function attr(input) { - const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)); - return attrs.map((attr) => ({ - name: "[" + cssesc(attr.name, { isIdentifier: true }) + "=\"" + cssesc(attr.value) + "\"]", - penalty: 0.5 - })); -} -function classNames(input) { - const names = Array.from(input.classList) - .filter(config.className); - return names.map((name) => ({ - name: "." + cssesc(name, { isIdentifier: true }), - penalty: 1 - })); -} -function tagName(input) { - const name = input.tagName.toLowerCase(); - if (config.tagName(name)) { - return { - name, - penalty: 2 - }; - } - return null; -} -function any() { - return { - name: "*", - penalty: 3 - }; -} -function index(input) { - const parent = input.parentNode; - if (!parent) { - return null; - } - let child = parent.firstChild; - if (!child) { - return null; - } - let i = 0; - while (child) { - if (child.nodeType === Node.ELEMENT_NODE) { - i++; - } - if (child === input) { - break; - } - child = child.nextSibling; - } - return i; -} -function nthChild(node, i) { - return { - name: node.name + `:nth-child(${i})`, - penalty: node.penalty + 1 - }; -} -function dispensableNth(node) { - return node.name !== "html" && !node.name.startsWith("#"); -} -function maybe(...level) { - const list = level.filter(notEmpty); - if (list.length > 0) { - return list; - } - return null; -} -function notEmpty(value) { - return value !== null && value !== undefined; -} -function* combinations(stack, path = []) { - if (stack.length > 0) { - for (let node of stack[0]) { - yield* combinations(stack.slice(1, stack.length), path.concat(node)); - } - } - else { - yield path; - } -} -function sort(paths) { - return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); -} -function* optimize(path, input, scope = { - counter: 0, - visited: new Map() -}) { - if (path.length > 2 && path.length > config.optimizedMinLength) { - for (let i = 1; i < path.length - 1; i++) { - if (scope.counter > config.maxNumberOfTries) { - return; // Okay At least I tried! - } - scope.counter += 1; - const newPath = [...path]; - newPath.splice(i, 1); - const newPathKey = selector(newPath); - if (scope.visited.has(newPathKey)) { - return; - } - if (unique(newPath) && same(newPath, input)) { - yield newPath; - scope.visited.set(newPathKey, true); - yield* optimize(newPath, input, scope); - } - } - } -} -function same(path, input) { - return rootDocument.querySelector(selector(path)) === input; -} -const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; -const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; -const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; -const defaultOptions = { - "escapeEverything": false, - "isIdentifier": false, - "quotes": "single", - "wrap": false -}; -function cssesc(string, opt = {}) { - const options = Object.assign(Object.assign({}, defaultOptions), opt); - if (options.quotes != "single" && options.quotes != "double") { - options.quotes = "single"; - } - const quote = options.quotes == "double" ? "\"" : "'"; - const isIdentifier = options.isIdentifier; - const firstChar = string.charAt(0); - let output = ""; - let counter = 0; - const length = string.length; - while (counter < length) { - const character = string.charAt(counter++); - let codePoint = character.charCodeAt(0); - let value = void 0; - // If it’s not a printable ASCII character… - if (codePoint < 0x20 || codePoint > 0x7E) { - if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) { - // It’s a high surrogate, and there is a next character. - const extra = string.charCodeAt(counter++); - if ((extra & 0xFC00) == 0xDC00) { - // next character is low surrogate - codePoint = ((codePoint & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000; - } - else { - // It’s an unmatched surrogate; only append this code unit, in case - // the next code unit is the high surrogate of a surrogate pair. - counter--; - } - } - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - else { - if (options.escapeEverything) { - if (regexAnySingleEscape.test(character)) { - value = "\\" + character; - } - else { - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - } - else if (/[\t\n\f\r\x0B]/.test(character)) { - value = "\\" + codePoint.toString(16).toUpperCase() + " "; - } - else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) { - value = "\\" + character; - } - else { - value = character; - } - } - output += value; - } - if (isIdentifier) { - if (/^-[-\d]/.test(output)) { - output = "\\-" + output.slice(1); - } - else if (/\d/.test(firstChar)) { - output = "\\3" + firstChar + " " + output.slice(1); - } - } - // Remove spaces after `\HEX` escapes that are not followed by a hex digit, - // since they’re redundant. Note that this is only possible if the escape - // sequence isn’t preceded by an odd number of backslashes. - output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { - if ($1 && $1.length % 2) { - // It’s not safe to remove the space, so don’t. - return $0; - } - // Strip the space. - return ($1 || "") + $2; - }); - if (!isIdentifier && options.wrap) { - return quote + output + quote; - } - return output; -} diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts index bb2621d75..fc9f64af2 100644 --- a/tracker/tracker/src/main/vendors/finder/finder.ts +++ b/tracker/tracker/src/main/vendors/finder/finder.ts @@ -279,14 +279,16 @@ function notEmpty(value: T | null | undefined): value is T { return value !== null && value !== undefined } -function* combinations(stack: Node[][], path: Node[] = []): Generator { +function combinations(stack: Node[][], path: Node[] = []): Node[][] { + const paths: Node[][] = [] if (stack.length > 0) { for (let node of stack[0]) { - yield* combinations(stack.slice(1, stack.length), path.concat(node)) + paths.push(...combinations(stack.slice(1, stack.length), path.concat(node))) } } else { - yield path + paths.push(path) } + return paths } function sort(paths: Iterable): Path[] { @@ -298,29 +300,31 @@ type Scope = { visited: Map } -function* optimize(path: Path, input: Element, scope: Scope = { +function optimize(path: Path, input: Element, scope: Scope = { counter: 0, visited: new Map() -}): Generator { +}): Node[][] { + const paths: Node[][] = [] if (path.length > 2 && path.length > config.optimizedMinLength) { for (let i = 1; i < path.length - 1; i++) { if (scope.counter > config.maxNumberOfTries) { - return // Okay At least I tried! + return paths // Okay At least I tried! } scope.counter += 1 const newPath = [...path] newPath.splice(i, 1) const newPathKey = selector(newPath) if (scope.visited.has(newPathKey)) { - return + return paths } if (unique(newPath) && same(newPath, input)) { - yield newPath + paths.push(newPath) scope.visited.set(newPathKey, true) - yield* optimize(newPath, input, scope) + paths.push(...optimize(newPath, input, scope)) } } } + return paths } function same(path: Path, input: Element) { diff --git a/tracker/tracker/src/messages/index.ts b/tracker/tracker/src/messages/index.ts index 487868202..210f534cb 100644 --- a/tracker/tracker/src/messages/index.ts +++ b/tracker/tracker/src/messages/index.ts @@ -885,3 +885,19 @@ export const MouseClick = bindNew(_MouseClick); classes.set(69, MouseClick); +class _CreateIFrameDocument implements Message { + readonly _id: number = 70; + constructor( + public frameID: number, + public id: number + ) {} + encode(writer: Writer): boolean { + return writer.uint(70) && + writer.uint(this.frameID) && + writer.uint(this.id); + } +} +export const CreateIFrameDocument = bindNew(_CreateIFrameDocument); +classes.set(70, CreateIFrameDocument); + + diff --git a/tracker/tracker/src/messages/writer.ts b/tracker/tracker/src/messages/writer.ts index 5ce52d330..6947420bc 100644 --- a/tracker/tracker/src/messages/writer.ts +++ b/tracker/tracker/src/messages/writer.ts @@ -77,6 +77,9 @@ export default class Writer { return this.offset <= this.size; } uint(value: number): boolean { + if (value < 0 || value > Number.MAX_SAFE_INTEGER) { + value = 0 + } while (value >= 0x80) { this.data[this.offset++] = value % 0x100 | 0x80; value = Math.floor(value / 128); diff --git a/tracker/tracker/src/webworker/index.ts b/tracker/tracker/src/webworker/index.ts index 1c6cde40f..d680bfab3 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -49,7 +49,7 @@ function sendBatch(batch: Uint8Array):void { if (this.status >= 400) { // TODO: test workflow. After 400+ it calls /start for some reason reset(); sendQueue.length = 0; - if (this.status === 403) { // Unauthorised (Token expired) + if (this.status === 401) { // Unauthorised (Token expired) self.postMessage("restart") return } @@ -74,6 +74,7 @@ function sendBatch(batch: Uint8Array):void { attemptsCount++; setTimeout(() => sendBatch(batch), ATTEMPT_TIMEOUT); } + // TODO: handle offline exception req.send(batch.buffer); } diff --git a/utilities/package-lock.json b/utilities/package-lock.json index 3de98ac06..75a02b0bd 100644 --- a/utilities/package-lock.json +++ b/utilities/package-lock.json @@ -1,8 +1,1031 @@ { "name": "utilities_server", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "utilities_server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.992.0", + "express": "^4.17.1", + "peer": "^0.6.1", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" + }, + "node_modules/@types/express": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz", + "integrity": "sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/node": { + "version": "15.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", + "integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" + }, + "node_modules/@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "node_modules/@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/aws-sdk": { + "version": "2.992.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.992.0.tgz", + "integrity": "sha512-FP/AOu1nxfaPJ6to05eHriBUzvPiNapEwy96sm5GNOL8/T38k9//H6UhxLJ/46CzxFMH/Mo/WFp0qwpS39ev5A==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dependencies": { + "mime-db": "1.48.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/peer": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/peer/-/peer-0.6.1.tgz", + "integrity": "sha512-zPJSPoZvo+83sPJNrW8o93QTktx7dKk67965RRDDNAIelWw1ZwE6ZmmhsvRrdNRlK0knQb3rR8GBdZlbWzCYJw==", + "dependencies": { + "@types/cors": "^2.8.6", + "@types/express": "^4.17.3", + "@types/ws": "^7.2.3", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "uuid": "^3.4.0", + "ws": "^7.2.3", + "yargs": "^15.3.1" + }, + "bin": { + "peerjs": "bin/peerjs" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz", + "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + }, "dependencies": { "@types/body-parser": { "version": "1.19.0", @@ -112,9 +1135,9 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "aws-sdk": { - "version": "2.932.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.932.0.tgz", - "integrity": "sha512-U6MWUtFD0npWa+ReVEgm0fCIM0fMOYahFp14GLv8fC+BWOTvh5Iwt/gF8NrLomx42bBjA1Abaw6yhmiaSJDQHQ==", + "version": "2.992.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.992.0.tgz", + "integrity": "sha512-FP/AOu1nxfaPJ6to05eHriBUzvPiNapEwy96sm5GNOL8/T38k9//H6UhxLJ/46CzxFMH/Mo/WFp0qwpS39ev5A==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -727,7 +1750,8 @@ "ws": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz", - "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==" + "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==", + "requires": {} }, "xml2js": { "version": "0.4.19", diff --git a/utilities/package.json b/utilities/package.json index d0cfcdbcc..da4c927d9 100644 --- a/utilities/package.json +++ b/utilities/package.json @@ -18,7 +18,7 @@ }, "homepage": "https://github.com/openreplay/openreplay#readme", "dependencies": { - "aws-sdk": "^2.654.0", + "aws-sdk": "^2.992.0", "express": "^4.17.1", "peer": "^0.6.1", "source-map": "^0.7.3" diff --git a/utilities/servers/peerjs-server.js b/utilities/servers/peerjs-server.js index ef6556531..a103764c3 100644 --- a/utilities/servers/peerjs-server.js +++ b/utilities/servers/peerjs-server.js @@ -48,13 +48,13 @@ const peerError = (error) => { } -peerRouter.get('/peers', function (req, res) { +peerRouter.get(`/${process.env.S3_KEY}/peers`, function (req, res) { console.log("looking for all available sessions"); res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({"data": connectedPeers})); }); -peerRouter.get('/peers/:projectKey', function (req, res) { +peerRouter.get(`/${process.env.S3_KEY}/peers/:projectKey`, function (req, res) { console.log(`looking for available sessions for ${req.params.projectKey}`); res.statusCode = 200; res.setHeader('Content-Type', 'application/json');