diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..3a1b7ac6
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+**/.gradle
+**/build
+**/out
+**/.idea
+**/*.iml
+**/.DS_Store
+**/node_modules
+**/dist
+**/.screen-output
+**/.allure
+**/logs
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index ba83ba07..7c79eaf7 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -21,7 +21,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
- ref: ${{ env.GITHUB_SHA }}
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
@@ -60,7 +61,7 @@ jobs:
docker pull twilio/selenoid:chrome_stable_140
- name: Get the last commit message
run: |
- echo "HEAD_COMMIT_MESSAGE=$(git show -s --format=%s)" >> $GITHUB_ENV
+ echo "HEAD_COMMIT_MESSAGE=$(git log -1 --format=%s ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV
- name: Prepare screenshot dir
run: |
mkdir -p niffler-e-2-e-tests/.screen-output/screenshots/selenoid
diff --git a/README.md b/README.md
index ea90dd5b..2b83db32 100644
--- a/README.md
+++ b/README.md
@@ -75,12 +75,12 @@ User-MacBook-Pro ~ % docker -v
Docker version 20.10.14, build a224086
```
-#### 2. Спуллить контейнер postgres:15.1, zookeeper и kafka версии 7.3.2
+#### 2. Спуллить контейнер postgres:15.1, zookeeper и kafka версии 7.7.7
```posh
docker pull postgres:15.1
-docker pull confluentinc/cp-zookeeper:7.3.2
-docker pull confluentinc/cp-kafka:7.3.2
+docker pull confluentinc/cp-zookeeper:7.7.7
+docker pull confluentinc/cp-kafka:7.7.7
```
После `pull` вы увидите спуленный image командой `docker images`
@@ -89,8 +89,8 @@ docker pull confluentinc/cp-kafka:7.3.2
mitriis-MacBook-Pro ~ % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
postgres 15.1 9f3ec01f884d 10 days ago 379MB
-confluentinc/cp-kafka 7.3.2 db97697f6e28 12 months ago 457MB
-confluentinc/cp-zookeeper 7.3.2 6fe5551964f5 7 years ago 451MB
+confluentinc/cp-kafka 7.7.7 db97697f6e28 12 months ago 457MB
+confluentinc/cp-zookeeper 7.7.7 6fe5551964f5 7 years ago 451MB
```
@@ -100,6 +100,21 @@ confluentinc/cp-zookeeper 7.3.2 6fe5551964f5 7 years ago 451MB
docker volume create pgdata
```
+#### 4.0 Если у вас ОС Windows:
+
+Необходимо выполнить следующие команды в каталоге /postgres/script :
+```
+sed -i -e 's/\r$//' init-database.sh
+chmod +x init-database.sh
+```
+
+Также необходимо исправить команду в скрипте localenv.sh (в корне проекта):
+
+```diff
+- docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data -v ./postgres/script:/docker-entrypoint-initdb.d -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -d postgres:15.1 --max_prepared_transactions=100
++ docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data -v /$(pwd)/postgres/script:/docker-entrypoint-initdb.d -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -d postgres:15.1 --max_prepared_transactions=100
+```
+
#### 4. Запустить БД, zookeeper и kafka 3-мя последовательными командами:
Запустив скрипт (Для Windows необходимо использовать bash terminal: gitbash, cygwin или wsl)
@@ -113,44 +128,29 @@ User-MacBook-Pro niffler % bash localenv.sh
```posh
docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -v pgdata:/var/lib/postgresql/data -v ./postgres/script:/docker-entrypoint-initdb.d -d postgres:15.1 --max_prepared_transactions=100
-docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.3.2
+docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.7.7
docker run --name=kafka -e KAFKA_BROKER_ID=1 \
--e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format='{{ .NetworkSettings.IPAddress }}'):2181 \
+-e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format='{{ .NetworkSettings.Networks.bridge.IPAddress }}'):2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \
--p 9092:9092 -d confluentinc/cp-kafka:7.3.2
+-p 9092:9092 -d confluentinc/cp-kafka:7.7.7
```
Для Windows (Необходимо использовать bash terminal: gitbash, cygwin или wsl):
```posh
-docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -v pgdata:/var/lib/postgresql/data -v ./postgres/script:/docker-entrypoint-initdb.d -d postgres:15.1 --max_prepared_transactions=100
+docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -v pgdata:/var/lib/postgresql/data -v /$(pwd)/postgres/script:/docker-entrypoint-initdb.d -d postgres:15.1 --max_prepared_transactions=100
-docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.3.2
+docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.7.7
-docker run --name=kafka -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format="{{ .NetworkSettings.IPAddress }}"):2181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 -p 9092:9092 -d confluentinc/cp-kafka:7.3.2
+docker run --name=kafka -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format="{{ .NetworkSettings.Networks.bridge.IPAddress }}"):2181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 -p 9092:9092 -d confluentinc/cp-kafka:7.7.7
```
[Про IP zookeeper](https://github.com/confluentinc/cp-docker-images/issues/801#issuecomment-692085103)
-Если вы используете Windows и контейнер с БД не стартует с ошибкой в логе:
-
-```
-server started
-/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init-database.sh
-/usr/local/bin/docker-entrypoint.sh: /docker-entrypoint-initdb.d/init-database.sh: /bin/bash^M: bad interpreter: No such file or directory
-```
-
-То необходимо выполнить следующие команды в каталоге /postgres/script :
-
-```
-sed -i -e 's/\r$//' init-database.sh
-chmod +x init-database.sh
-```
-
#### 5. Установить Java версии 21. Это необходимо, т.к. проект использует синтаксис Java 21
Версию установленной Java необходимо проверить командой `java -version`
@@ -401,4 +401,4 @@ User-MacBook-Pro niffler % bash docker-compose-e2e.sh
#### 5. Allure report доступен по адресу: http://localhost:5050/allure-docker-service/projects/niffler-ng/reports/latest/index.html
-
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 7307753c..329476f3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,10 @@ plugins {
group = 'guru.qa'
+wrapper {
+ gradleVersion = '9.2.1'
+}
+
allprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
@@ -31,7 +35,7 @@ subprojects {
springOpenApiVersion = '2.8.13'
webauthn4jVersion = '0.29.7.RELEASE'
postgresDriverVersion = '42.7.8'
- flywayVersion = '11.14.1'
+ flywayVersion = '11.19.1'
springGrpcVersion = '3.1.0.RELEASE'
springGraphqlDateTimeVersion = '6.0.0'
springSecurityTestVersion = '6.5.6'
@@ -43,7 +47,7 @@ subprojects {
thumbnailatorVersion = '0.4.21'
apacheCsvVersion = '1.14.1'
h2Version = '2.4.240'
- mockitoVersion = '5.20.0'
+ mockitoVersion = '5.21.0'
dockerImage = System.getProperty("os.arch") == "aarch64" || System.getProperty("os.arch") == "arm64"
? "arm64v8/eclipse-temurin:21-jdk"
diff --git a/docker-compose.yml b/docker-compose.yml
index a6f4f230..f5580db8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -23,7 +23,7 @@ services:
zookeeper:
container_name: zookeeper
- image: confluentinc/cp-zookeeper:7.3.2
+ image: confluentinc/cp-zookeeper:7.7.7
ports:
- 2181:2181
environment:
@@ -33,7 +33,7 @@ services:
kafka:
container_name: kafka
- image: confluentinc/cp-kafka:7.3.2
+ image: confluentinc/cp-kafka:7.7.7
ports:
- 9092:9092
depends_on:
@@ -194,7 +194,7 @@ services:
- HEAD_COMMIT_MESSAGE=${HEAD_COMMIT_MESSAGE}
- EXECUTION_TYPE=${EXECUTION_TYPE}
volumes:
- - ./niffler-e-2-e-tests/.screen-output/screenshots/selenoid:/niffler/niffler-e-2-e-tests/.screen-output/screenshots/selenoid
+ - ./niffler-e-2-e-tests/.screen-output/screenshots/selenoid:/home/gradle/niffler/niffler-e-2-e-tests/.screen-output/screenshots/selenoid
depends_on:
frontend.niffler.dc:
condition: service_started
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ab521aa0..cd4b7aa8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/localenv.sh b/localenv.sh
index ea5cd392..67119a3c 100644
--- a/localenv.sh
+++ b/localenv.sh
@@ -4,11 +4,11 @@ docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)
docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data -v ./postgres/script:/docker-entrypoint-initdb.d -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -d postgres:15.1 --max_prepared_transactions=100
-docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.3.2
+docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.7.7
docker run --name=kafka -e KAFKA_BROKER_ID=1 \
--e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format='{{ .NetworkSettings.IPAddress }}'):2181 \
+-e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format='{{ .NetworkSettings.Networks.bridge.IPAddress }}'):2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \
--p 9092:9092 -d confluentinc/cp-kafka:7.3.2
+-p 9092:9092 -d confluentinc/cp-kafka:7.7.7
diff --git a/niffler-auth/build.gradle b/niffler-auth/build.gradle
index 7dada94c..5aee0983 100644
--- a/niffler-auth/build.gradle
+++ b/niffler-auth/build.gradle
@@ -1,10 +1,10 @@
plugins {
- id 'org.springframework.boot' version '3.5.6'
+ id 'org.springframework.boot' version '3.5.8'
id 'com.google.cloud.tools.jib' version '3.4.1'
}
group = 'guru.qa'
-version = '2.5.3'
+version = '2.5.4'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
diff --git a/niffler-currency/build.gradle b/niffler-currency/build.gradle
index 7be456ea..9a258d24 100644
--- a/niffler-currency/build.gradle
+++ b/niffler-currency/build.gradle
@@ -1,10 +1,10 @@
plugins {
- id 'org.springframework.boot' version '3.5.6'
+ id 'org.springframework.boot' version '3.5.8'
id 'com.google.cloud.tools.jib' version '3.4.1'
}
group = 'guru.qa'
-version = '2.0.6'
+version = '2.0.7'
dependencies {
implementation project(':niffler-grpc-common')
diff --git a/niffler-e-2-e-tests/Dockerfile b/niffler-e-2-e-tests/Dockerfile
index 704cf291..ded1abf8 100644
--- a/niffler-e-2-e-tests/Dockerfile
+++ b/niffler-e-2-e-tests/Dockerfile
@@ -1,13 +1,11 @@
-FROM eclipse-temurin:21-jdk
+FROM gradle:9.2.1-jdk21
-WORKDIR /niffler
+WORKDIR /home/gradle/niffler
ENV TZ=Europe/Moscow
-COPY ./gradle ./gradle
COPY ./niffler-e-2-e-tests ./niffler-e-2-e-tests
COPY ./niffler-grpc-common ./niffler-grpc-common
-COPY ./gradlew ./
COPY ./build.gradle ./
COPY ./settings.gradle ./
COPY ./gradle.properties ./
-CMD ./gradlew test -p niffler-e-2-e-tests -Dtest.env=docker -Drepository=jpa -Duser.timezone=Europe/Moscow
+CMD gradle test -p niffler-e-2-e-tests -Dtest.env=docker -Drepository=jpa -Duser.timezone=Europe/Moscow
diff --git a/niffler-e-2-e-tests/build.gradle b/niffler-e-2-e-tests/build.gradle
index db674beb..f7411dbf 100644
--- a/niffler-e-2-e-tests/build.gradle
+++ b/niffler-e-2-e-tests/build.gradle
@@ -1,21 +1,22 @@
buildscript {
ext {
- junitVersion = '5.13.4'
- allureVersion = '2.30.0'
- selenideVersion = '7.11.1'
- okhttp3Version = '5.2.1'
+ junitVersion = '5.14.1'
+ allureVersion = '2.32.0'
+ aspectJVersion = '1.9.22'
+ selenideVersion = '7.13.0'
+ okhttp3Version = '5.3.2'
retrofitVersion = '3.0.0'
apollographqlVersion = '0.0.2'
- logbackVersion = '1.5.20'
+ logbackVersion = '1.5.22'
slf4jVersion = '2.0.17'
springJdbcVersion = '6.2.12'
springCryptoVersion = '6.5.6'
p6spyVersion = '3.9.1'
- hibernateVersion = '7.1.1.Final'
- kafkaClientsVersion = '4.1.0'
+ hibernateVersion = '7.2.0.Final'
+ kafkaClientsVersion = '4.1.1'
saajVersion = '3.0.4'
springDataCommonsVersion = '3.5.5'
- wiremockVersion = '3.13.1'
+ wiremockVersion = '3.13.2'
ashotVersion = '1.5.4'
fakerVersion = '1.0.2'
userdataWsdlUrl = System.getProperty("test.env") == "docker"
@@ -28,20 +29,19 @@ buildscript {
}
plugins {
- id 'io.qameta.allure' version '3.0.0'
- id 'io.qameta.allure-adapter' version '3.0.0'
+ id 'io.qameta.allure' version '3.0.1'
+ id 'io.qameta.allure-adapter' version '3.0.1'
id 'com.github.edeandrea.xjc-generation' version '1.6'
id 'com.apollographql.apollo' version '4.3.3'
}
group = 'guru.qa'
-version = '2.1.4'
+version = '2.1.5'
-allure {
- version.set("${allureVersion}")
- adapter {
- aspectjVersion.set("1.9.22")
- aspectjWeaver.set(true)
+configurations {
+ agent {
+ canBeResolved = true
+ canBeConsumed = true
}
}
@@ -87,7 +87,8 @@ dependencies {
testImplementation "ch.qos.logback:logback-classic:${logbackVersion}"
testImplementation "org.slf4j:slf4j-api:${slf4jVersion}"
// JUnit
- testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
+ testImplementation platform("org.junit:junit-bom:${junitVersion}")
+ testImplementation "org.junit.jupiter:junit-jupiter"
// REST
testImplementation "com.squareup.okhttp3:logging-interceptor:${okhttp3Version}"
testImplementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttp3Version}"
@@ -111,15 +112,17 @@ dependencies {
testImplementation "org.hibernate:hibernate-core:${hibernateVersion}"
testImplementation "org.springframework.security:spring-security-crypto:${springCryptoVersion}"
// Allure
- testImplementation "io.qameta.allure:allure-attachments:${allureVersion}"
- testImplementation("io.qameta.allure:allure-junit5:${allureVersion}") {
+ agent "org.aspectj:aspectjweaver:${aspectJVersion}"
+ testImplementation platform("io.qameta.allure:allure-bom:$allureVersion")
+ testImplementation "io.qameta.allure:allure-attachments"
+ testImplementation "io.qameta.allure:allure-okhttp3"
+ testImplementation "io.qameta.allure:allure-grpc"
+ testImplementation("io.qameta.allure:allure-junit5") {
exclude group: "org.junit.jupiter"
}
- testImplementation("io.qameta.allure:allure-selenide:${allureVersion}") {
+ testImplementation("io.qameta.allure:allure-selenide") {
exclude group: 'com.codeborne'
}
- testImplementation "io.qameta.allure:allure-okhttp3:${allureVersion}"
- testImplementation "io.qameta.allure:allure-grpc:${allureVersion}"
// WEB
testImplementation "com.codeborne:selenide:${selenideVersion}"
testImplementation "ru.yandex.qatools.ashot:ashot:${ashotVersion}"
@@ -127,7 +130,7 @@ dependencies {
testImplementation("com.github.javafaker:javafaker:${fakerVersion}") {
exclude group: 'org.yaml'
}
- testImplementation 'commons-io:commons-io:2.20.0'
+ testImplementation 'commons-io:commons-io:2.21.0'
testImplementation 'com.google.code.findbugs:jsr305:3.0.2'
testImplementation 'com.github.vertical-blank:sql-formatter:2.0.5'
// gRPC
@@ -167,4 +170,5 @@ tasks.named { it == "generateNifflerApolloSources" }.configureEach {
test {
dependsOn("generateNifflerApolloSources")
+ jvmArgs = [ "-javaagent:${configurations.agent.singleFile}" ]
}
diff --git a/niffler-gateway/build.gradle b/niffler-gateway/build.gradle
index 7fd9f2a4..d6f7fd91 100644
--- a/niffler-gateway/build.gradle
+++ b/niffler-gateway/build.gradle
@@ -1,11 +1,11 @@
plugins {
- id 'org.springframework.boot' version '3.5.6'
+ id 'org.springframework.boot' version '3.5.8'
id 'com.google.cloud.tools.jib' version '3.4.1'
id 'com.github.edeandrea.xjc-generation' version '1.6'
}
group = 'guru.qa'
-version = '2.4.2'
+version = '2.4.3'
dependencies {
implementation project(':niffler-grpc-common')
diff --git a/niffler-ng-client/Dockerfile b/niffler-ng-client/Dockerfile
index 35709869..12bbf0c7 100644
--- a/niffler-ng-client/Dockerfile
+++ b/niffler-ng-client/Dockerfile
@@ -2,8 +2,8 @@ FROM node:22.6.0-alpine AS build
ARG NPM_COMMAND
ARG VERSION
WORKDIR /app
-COPY package.json ./
-RUN npm install
+COPY package.json package-lock.json ./
+RUN npm ci --no-audit --no-fund
COPY . ./
RUN npm run ${NPM_COMMAND}
diff --git a/niffler-spend/build.gradle b/niffler-spend/build.gradle
index 3d66ae85..44bc8546 100644
--- a/niffler-spend/build.gradle
+++ b/niffler-spend/build.gradle
@@ -1,10 +1,10 @@
plugins {
- id 'org.springframework.boot' version '3.5.6'
+ id 'org.springframework.boot' version '3.5.8'
id 'com.google.cloud.tools.jib' version '3.4.1'
}
group = 'guru.qa'
-version = '2.2.2'
+version = '2.2.3'
dependencies {
implementation project(':niffler-grpc-common')
diff --git a/niffler-userdata/build.gradle b/niffler-userdata/build.gradle
index 231cfdf8..700388fc 100644
--- a/niffler-userdata/build.gradle
+++ b/niffler-userdata/build.gradle
@@ -1,11 +1,11 @@
plugins {
- id 'org.springframework.boot' version '3.5.6'
+ id 'org.springframework.boot' version '3.5.8'
id 'com.google.cloud.tools.jib' version '3.4.1'
id 'com.github.edeandrea.xjc-generation' version '1.6'
}
group = 'guru.qa'
-version = '2.2.2'
+version = '2.2.3'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'