diff --git a/.env b/.env index f77083a035..cab8336e30 100644 --- a/.env +++ b/.env @@ -4,12 +4,13 @@ _APP_WORKER_PER_CORE=6 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= +_APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io _APP_SYSTEM_EMAIL_NAME=Appwrite _APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io _APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io _APP_SYSTEM_RESPONSE_FORMAT= _APP_OPTIONS_ABUSE=disabled -_APP_OPTIONS_ROUTER_PROTECTION=disbled +_APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key @@ -50,10 +51,6 @@ _APP_STORAGE_WASABI_BUCKET= _APP_STORAGE_ANTIVIRUS=disabled _APP_STORAGE_ANTIVIRUS_HOST=clamav _APP_STORAGE_ANTIVIRUS_PORT=3310 -_APP_INFLUXDB_HOST=influxdb -_APP_INFLUXDB_PORT=8086 -_APP_STATSD_HOST=telegraf -_APP_STATSD_PORT=8125 _APP_SMTP_HOST=maildev _APP_SMTP_PORT=1025 _APP_SMTP_SECURE= @@ -79,7 +76,7 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 -_APP_USAGE_AGGREGATION_INTERVAL=5 +_APP_USAGE_AGGREGATION_INTERVAL=60000 _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 _APP_MAINTENANCE_RETENTION_SCHEDULES=86400 _APP_USAGE_STATS=enabled diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14e1ac5e44..9c9b678302 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,8 +75,32 @@ jobs: - name: Run Unit Tests run: docker compose exec appwrite test /usr/src/code/tests/unit - e2e_test: - name: E2E Test + e2e_general_test: + name: E2E General Test + runs-on: ubuntu-latest + needs: setup + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Load Cache + uses: actions/cache@v3 + with: + key: ${{ env.CACHE_KEY }} + path: /tmp/${{ env.IMAGE }}.tar + fail-on-cache-miss: true + + - name: Load and Start Appwrite + run: | + docker load --input /tmp/${{ env.IMAGE }}.tar + docker compose up -d + sleep 10 + + - name: Run General Tests + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug + + e2e_service_test: + name: E2E Service Test runs-on: ubuntu-latest needs: setup strategy: @@ -120,4 +144,4 @@ jobs: sleep 10 - name: Run ${{matrix.service}} Tests - run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug \ No newline at end of file + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug diff --git a/CHANGES.md b/CHANGES.md index 5634340d69..5dd4ba8770 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +# Version 1.4.14 + +## Changes +- New usage metrics collection flow [#7005](https://github.com/appwrite/appwrite/pull/7005) + - Deprecated influxdb, telegraf containers and removed all of their occurrences from the code. + - Removed _APP_INFLUXDB_HOST, _APP_INFLUXDB_PORT, _APP_STATSD_HOST, _APP_STATSD_PORT env variables. + - Removed usage labels dependency. + - Dropped type attribute from stats collection. + - Usage metrics are processed via new usage worker. + - updated Metric names. + # Version 1.4.13 ## Notable changes @@ -49,6 +60,7 @@ * Use getQueueSize() in the Health service's get X queue endpoints [#7073](https://github.com/appwrite/appwrite/pull/7073) * Delete linked VCS repos and comments [#7066](https://github.com/appwrite/appwrite/pull/7066) + # Version 1.4.9 ## Bug fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccd61e742b..0522e33da5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -210,7 +210,6 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi │ ├── Task │ ├── Template │ ├── URL -│ ├── Usage │ └── Utopia └── tests # End to end & unit tests ├── e2e diff --git a/Dockerfile b/Dockerfile index 2f85f2cc43..7c37d4a4f1 100755 --- a/Dockerfile +++ b/Dockerfile @@ -71,43 +71,56 @@ RUN mkdir -p /storage/uploads && \ chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \ chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug +# Development Executables +RUN chmod +x /usr/local/bin/dev-generate-translations + # Executables RUN chmod +x /usr/local/bin/doctor && \ - chmod +x /usr/local/bin/maintenance && \ - chmod +x /usr/local/bin/usage && \ chmod +x /usr/local/bin/install && \ - chmod +x /usr/local/bin/upgrade && \ + chmod +x /usr/local/bin/maintenance && \ chmod +x /usr/local/bin/migrate && \ chmod +x /usr/local/bin/realtime && \ - chmod +x /usr/local/bin/schedule && \ + chmod +x /usr/local/bin/schedule-functions && \ + chmod +x /usr/local/bin/schedule-messages && \ chmod +x /usr/local/bin/sdks && \ chmod +x /usr/local/bin/specs && \ chmod +x /usr/local/bin/ssl && \ chmod +x /usr/local/bin/test && \ + chmod +x /usr/local/bin/upgrade && \ chmod +x /usr/local/bin/vars && \ chmod +x /usr/local/bin/worker-audits && \ + chmod +x /usr/local/bin/worker-builds && \ chmod +x /usr/local/bin/worker-certificates && \ chmod +x /usr/local/bin/worker-databases && \ chmod +x /usr/local/bin/worker-deletes && \ chmod +x /usr/local/bin/worker-functions && \ - chmod +x /usr/local/bin/worker-builds && \ + chmod +x /usr/local/bin/worker-hamster && \ chmod +x /usr/local/bin/worker-mails && \ chmod +x /usr/local/bin/worker-messaging && \ - chmod +x /usr/local/bin/worker-webhooks && \ chmod +x /usr/local/bin/worker-migrations && \ - chmod +x /usr/local/bin/worker-hamster + chmod +x /usr/local/bin/worker-webhooks && \ + chmod +x /usr/local/bin/worker-hamster && \ + chmod +x /usr/local/bin/worker-usage + # Cloud Executabless -RUN chmod +x /usr/local/bin/hamster && \ - chmod +x /usr/local/bin/volume-sync && \ +RUN chmod +x /usr/local/bin/calc-tier-stats && \ + chmod +x /usr/local/bin/calc-users-stats && \ + chmod +x /usr/local/bin/clear-card-cache && \ + chmod +x /usr/local/bin/delete-orphaned-projects && \ + chmod +x /usr/local/bin/get-migration-stats && \ + chmod +x /usr/local/bin/hamster && \ + chmod +x /usr/local/bin/patch-delete-project-collections && \ chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \ chmod +x /usr/local/bin/patch-recreate-repositories-documents && \ + chmod +x /usr/local/bin/volume-sync && \ chmod +x /usr/local/bin/patch-delete-project-collections && \ chmod +x /usr/local/bin/delete-orphaned-projects && \ chmod +x /usr/local/bin/clear-card-cache && \ chmod +x /usr/local/bin/calc-users-stats && \ chmod +x /usr/local/bin/calc-tier-stats && \ - chmod +x /usr/local/bin/get-migration-stats + chmod +x /usr/local/bin/get-migration-stats && \ + chmod +x /usr/local/bin/create-inf-metric # Letsencrypt Permissions RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ diff --git a/app/cli.php b/app/cli.php index e4ecf17f3c..668b70d8b7 100644 --- a/app/cli.php +++ b/app/cli.php @@ -125,30 +125,6 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, }; }, ['pools', 'dbForConsole', 'cache']); -CLI::setResource('influxdb', function (Registry $register) { - $client = $register->get('influxdb'); /** @var InfluxDB\Client $client */ - $attempts = 0; - $max = 10; - $sleep = 1; - - do { // check if telegraf database is ready - try { - $attempts++; - $database = $client->selectDB('telegraf'); - if (in_array('telegraf', $client->listDatabases())) { - break; // leave the do-while if successful - } - } catch (\Throwable $th) { - Console::warning("InfluxDB not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('InfluxDB database not ready yet'); - } - sleep($sleep); - } - } while ($attempts < $max); - return $database; -}, ['register']); - CLI::setResource('queue', function (Group $pools) { return $pools->get('queue')->pop()->getResource(); }, ['pools']); diff --git a/app/config/auth.php b/app/config/auth.php index cf1d180aaa..2330fe75cf 100644 --- a/app/config/auth.php +++ b/app/config/auth.php @@ -7,21 +7,28 @@ return [ 'name' => 'Email/Password', 'key' => 'emailPassword', 'icon' => '/images/users/email.png', - 'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateEmailSession', + 'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateEmailPasswordSession', 'enabled' => true, ], 'magic-url' => [ 'name' => 'Magic URL', 'key' => 'usersAuthMagicURL', 'icon' => '/images/users/magic-url.png', - 'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateMagicURLSession', + 'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateMagicURLToken', + 'enabled' => true, + ], + 'email-otp' => [ + 'name' => 'Email (OTP)', + 'key' => 'emailOtp', + 'icon' => '/images/users/email.png', + 'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateEmailToken', 'enabled' => true, ], 'anonymous' => [ 'name' => 'Anonymous', 'key' => 'anonymous', 'icon' => '/images/users/anonymous.png', - 'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateAnonymousSession', + 'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateAnonymousSession', 'enabled' => true, ], 'invites' => [ @@ -42,7 +49,7 @@ return [ 'name' => 'Phone', 'key' => 'phone', 'icon' => '/images/users/phone.png', - 'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreatePhoneSession', + 'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreatePhoneToken', 'enabled' => true, ], ]; diff --git a/app/config/collections.php b/app/config/collections.php index b3e3417555..43954d667c 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -18,6 +18,63 @@ $auth = Config::getParam('auth', []); */ $commonCollections = [ + 'cache' => [ + '$collection' => Database::METADATA, + '$id' => 'cache', + 'name' => 'Cache', + 'attributes' => [ + [ + '$id' => 'resource', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'accessedAt', + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => 'signature', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => '_key_accessedAt', + 'type' => Database::INDEX_KEY, + 'attributes' => ['accessedAt'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => '_key_resource', + 'type' => Database::INDEX_KEY, + 'attributes' => ['resource'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], + 'users' => [ '$collection' => ID::custom(Database::METADATA), '$id' => ID::custom('users'), @@ -681,6 +738,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('expire'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], ], 'indexes' => [ [ @@ -1281,10 +1349,10 @@ $commonCollections = [ ] ], - 'stats' => [ + 'stats_v2' => [ '$collection' => ID::custom(Database::METADATA), - '$id' => ID::custom('stats'), - 'name' => 'Stats', + '$id' => ID::custom('stats_v2'), + 'name' => 'stats_v2', 'attributes' => [ [ '$id' => ID::custom('metric'), @@ -1313,7 +1381,7 @@ $commonCollections = [ 'type' => Database::VAR_INTEGER, 'format' => '', 'size' => 8, - 'signed' => false, + 'signed' => true, 'required' => true, 'default' => null, 'array' => false, @@ -1341,17 +1409,6 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => ID::custom('type'), - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 1, - 'signed' => false, - 'required' => true, - 'default' => 0, // 0 -> count, 1 -> sum - 'array' => false, - 'filters' => [], - ], ], 'indexes' => [ [ @@ -1370,7 +1427,7 @@ $commonCollections = [ ], [ '$id' => ID::custom('_key_metric_period_time'), - 'type' => Database::INDEX_KEY, + 'type' => Database::INDEX_UNIQUE, 'attributes' => ['metric', 'period', 'time'], 'lengths' => [], 'orders' => [Database::ORDER_DESC], @@ -1486,7 +1543,7 @@ $commonCollections = [ [ '$id' => ID::custom('_key_enabled_type'), 'type' => Database::INDEX_KEY, - 'attributes' => ['enabled','type'], + 'attributes' => ['enabled', 'type'], 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], @@ -1593,6 +1650,28 @@ $commonCollections = [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('scheduleInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('scheduleId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('deliveredAt'), 'type' => Database::VAR_DATETIME, @@ -1810,6 +1889,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1853,7 +1943,14 @@ $commonCollections = [ 'attributes' => ['topicInternalId'], 'lengths' => [], 'orders' => [], - ] + ], + [ + '$id' => ID::custom('_fulltext_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], ], ], @@ -3495,63 +3592,6 @@ $projectCollections = array_merge([ ], ], - 'cache' => [ - '$collection' => Database::METADATA, - '$id' => 'cache', - 'name' => 'Cache', - 'attributes' => [ - [ - '$id' => 'resource', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 255, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'accessedAt', - 'type' => Database::VAR_DATETIME, - 'format' => '', - 'size' => 0, - 'signed' => false, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], - ], - [ - '$id' => 'signature', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 255, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - ], - 'indexes' => [ - [ - '$id' => '_key_accessedAt', - 'type' => Database::INDEX_KEY, - 'attributes' => ['accessedAt'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_resource', - 'type' => Database::INDEX_KEY, - 'attributes' => ['resource'], - 'lengths' => [], - 'orders' => [], - ], - ], - ], - 'variables' => [ '$collection' => Database::METADATA, '$id' => 'variables', @@ -4120,6 +4160,17 @@ $consoleCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('resourceCollection'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('resourceInternalId'), 'type' => Database::VAR_STRING, @@ -4526,6 +4577,39 @@ $consoleCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('enabled'), + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => false, + 'default' => true, + 'array' => false, + ], + [ + '$id' => ID::custom('logs'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 1000000, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('attempts'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/config/errors.php b/app/config/errors.php index 2e35bfb881..e5c2d4e5bd 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -4,6 +4,7 @@ * List of server wide error codes and their respective messages. */ +use Appwrite\Enum\MessageStatus; use Appwrite\Extend\Exception; return [ @@ -807,7 +808,7 @@ return [ ], Exception::PROVIDER_INCORRECT_TYPE => [ 'name' => Exception::PROVIDER_INCORRECT_TYPE, - 'description' => 'Provider with the requested ID is of incorrect type: ', + 'description' => 'Provider with the requested ID is of the incorrect type.', 'code' => 400, ], @@ -858,18 +859,27 @@ return [ ], Exception::MESSAGE_TARGET_NOT_EMAIL => [ 'name' => Exception::MESSAGE_TARGET_NOT_EMAIL, - 'description' => 'Message with the target ID is not an email target:', + 'description' => 'Message with the target ID is not an email target.', 'code' => 400, ], Exception::MESSAGE_TARGET_NOT_SMS => [ 'name' => Exception::MESSAGE_TARGET_NOT_SMS, - 'description' => 'Message with the target ID is not an SMS target:', + 'description' => 'Message with the target ID is not an SMS target.', 'code' => 400, ], Exception::MESSAGE_TARGET_NOT_PUSH => [ 'name' => Exception::MESSAGE_TARGET_NOT_PUSH, - 'description' => 'Message with the target ID is not a push target:', + 'description' => 'Message with the target ID is not a push target.', 'code' => 400, ], - + Exception::MESSAGE_MISSING_SCHEDULE => [ + 'name' => Exception::MESSAGE_MISSING_SCHEDULE, + 'description' => 'Message can not have status ' . MessageStatus::SCHEDULED . ' without a schedule.', + 'code' => 400, + ], + Exception::SCHEDULE_NOT_FOUND => [ + 'name' => Exception::SCHEDULE_NOT_FOUND, + 'description' => 'Schedule with the requested ID could not be found.', + 'code' => 404, + ], ]; diff --git a/app/config/events.php b/app/config/events.php index b0db9090fb..5378502faf 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -58,6 +58,14 @@ return [ '$description' => 'This event triggers when a user\'s target is deleted.', ], ], + 'tokens' => [ + '$model' => Response::MODEL_TOKEN, + '$resource' => true, + '$description' => 'This event triggers on any user\'s token event.', + 'create' => [ + '$description' => 'This event triggers when a user\'s token is created.', + ], + ], 'create' => [ '$description' => 'This event triggers when a user is created.' ], diff --git a/app/config/locale/languages.php b/app/config/locale/languages.php index 6272bd02a6..eeea92e636 100644 --- a/app/config/locale/languages.php +++ b/app/config/locale/languages.php @@ -654,10 +654,15 @@ return [ "nativeName" => "پښتو" ], [ - "code" => "pt", + "code" => "pt-pt", "name" => "Portuguese", "nativeName" => "Português" ], + [ + "code" => "pt-br", + "name" => "Portuguese (Brazilian)", + "nativeName" => "Português" + ], [ "code" => "qu", "name" => "Quechua", @@ -919,9 +924,14 @@ return [ "nativeName" => "Cuengh / Tôô / 壮语" ], [ - "code" => "zh", - "name" => "Chinese", - "nativeName" => "中文" + "code" => "zh-cn", + "name" => "Chinese (Simplified)", + "nativeName" => "中国人" + ], + [ + "code" => "zh-tw", + "name" => "Chinese (Traditional)", + "nativeName" => "中國人" ], [ "code" => "zu", diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl new file mode 100644 index 0000000000..4d6972389e --- /dev/null +++ b/app/config/locale/templates/email-base-styled.tpl @@ -0,0 +1,207 @@ + + + + + + + + + + + +
+ + + + +
+ +
+ + + + + +
+

{{subject}}

+
+ + + + + +
+{{body}} +
+ + + + + +
+ + + + + + + +
+ + + + + +
+ + + + + + +
Terms +
|
+
Privacy
+

+ © {{year}} Appwrite | 251 Little Falls Drive, Wilmington 19808, + Delaware, United States +

+
+ + \ No newline at end of file diff --git a/app/config/locale/templates/email-base.tpl b/app/config/locale/templates/email-base.tpl index f41a9530e1..13056fd5ae 100644 --- a/app/config/locale/templates/email-base.tpl +++ b/app/config/locale/templates/email-base.tpl @@ -1,5 +1,74 @@ + + + + + + @@ -8,13 +77,14 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600&display=swap" rel="stylesheet">