Commit Graph

437 Commits

Author SHA1 Message Date
hsiegeln
d7eb700860 refactor: move PG cleanup to TenantDatabaseService, keep only ClickHouse
TenantDataCleanupService now handles only ClickHouse GDPR erasure;
the dropPostgresSchema private method is removed and the public method
renamed cleanupClickHouse(). VendorTenantService updated accordingly
with the TODO comment removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:17:00 +02:00
hsiegeln
c1458e4995 feat: create per-tenant PG database during provisioning, drop on delete
Inject TenantDatabaseService; call createTenantDatabase() at the start
of provisionAsync() (stores generated password on TenantEntity), and
dropTenantDatabase() in delete() before GDPR data erasure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:16:06 +02:00
hsiegeln
b79a7fe405 feat: construct per-tenant JDBC URL with currentSchema and ApplicationName
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:14:35 +02:00
hsiegeln
6d6c1f3562 feat: add TenantDatabaseService for per-tenant PG user+schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:13:34 +02:00
hsiegeln
0e3f383cf4 feat: add dbPassword to TenantProvisionRequest 2026-04-15 00:13:27 +02:00
hsiegeln
cd6dd1e5af feat: add dbPassword field to TenantEntity 2026-04-15 00:13:12 +02:00
hsiegeln
dfa2a6bfa2 feat: add db_password column to tenants table (V015) 2026-04-15 00:13:11 +02:00
hsiegeln
a7196ff4c1 docs: per-tenant PostgreSQL isolation implementation plan
8-task plan covering migration, entity change, TenantDatabaseService,
provisioner JDBC URL construction, VendorTenantService integration,
and TenantDataCleanupService refactor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:11:34 +02:00
hsiegeln
17c6723f7e docs: per-tenant PostgreSQL isolation design spec
Per-tenant PG users and schemas for DB-level data isolation.
Each tenant server gets its own credentials and currentSchema/ApplicationName
JDBC parameters, aligned with server team's commit 7a63135.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:08:35 +02:00
hsiegeln
91e93696ed fix: improve Infrastructure page readability
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 54s
Use Card and KpiStrip design system components, add database icons to
section headers, right-align numeric columns, replace text toggles with
chevron icons, and constrain max width to prevent ultra-wide stretching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:26:43 +02:00
hsiegeln
57e41e407c fix: remove "Open Server Dashboard" link from tenant sidebar
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 55s
The server dashboard link in the sidebar footer is premature — tenant
servers may not be provisioned yet and the link target depends on org
context that isn't always available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:16:39 +02:00
hsiegeln
bc46af5cea fix: use configured credentials for tenant schema cleanup
All checks were successful
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 39s
Same hardcoded dev credentials bug as InfrastructureService —
TenantDataCleanupService.dropPostgresSchema() used "cameleer"/"cameleer_dev"
instead of the provisioning properties, causing schema DROP to fail on
production installs during tenant deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:11:16 +02:00
hsiegeln
03fb414981 fix: use configured credentials for infrastructure PostgreSQL queries
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 43s
pgConnection() had hardcoded dev credentials ("cameleer"/"cameleer_dev")
instead of using the provisioning properties, causing "password
authentication failed" on production installs where the password is
generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:01:00 +02:00
hsiegeln
553ecc1490 fix: PowerShell installer fixes for Windows and Logto console login
All checks were successful
CI / build (push) Successful in 1m7s
CI / docker (push) Successful in 18s
Three issues fixed:

1. Docker socket: use /var/run/docker.sock instead of Windows named pipe
   (//./pipe/docker_engine) — Linux containers can't use named pipes.

2. FQDN detection: reverse-DNS lookup on host IPs to find the FQDN
   instead of relying on GetHostEntry which returns bare hostname on
   Windows machines with DNS-registered domain suffixes.

3. Reinstall path duplication: Push-Location/Pop-Location in the
   reinstall handler used try/catch without finally, so Pop-Location
   was skipped when docker compose wrote to stderr under
   ErrorActionPreference=Stop. CWD stayed in the install dir, causing
   the relative ./cameleer default to resolve to cameleer/cameleer.

4. Logto bootstrap: register admin-console redirect URIs and add the
   admin user to Logto's internal organizations (t-default, t-admin)
   with the admin role — both required for console login to work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:46:05 +02:00
hsiegeln
dec1c53d30 docs: track AGENTS.md for GitNexus code intelligence instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:04:06 +02:00
hsiegeln
ace6ad0cf2 fix: remove openssl dependency for password generation
All checks were successful
CI / build (push) Successful in 1m18s
CI / docker (push) Successful in 19s
Use /dev/urandom instead of openssl rand for generating random
passwords. Available on all Linux/macOS systems without requiring
openssl to be installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:58:11 +02:00
hsiegeln
4a67677158 fix: use correct compose service names in health checks
All checks were successful
CI / build (push) Successful in 1m19s
CI / docker (push) Successful in 33s
The verify_health functions passed short service names (postgres,
clickhouse, server, logto) but the actual compose services are
prefixed with cameleer-. This caused docker compose ps -q to return
empty, so health was never read and checks always timed out.

Also renamed server/server-ui service definitions to
cameleer-server/cameleer-server-ui for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:17:41 +02:00
hsiegeln
27c3f4d136 refactor: prefix all third-party service names with cameleer-
Some checks failed
CI / build (push) Successful in 1m59s
CI / docker (push) Successful in 1m6s
SonarQube Analysis / sonarqube (push) Failing after 1m55s
Rename all Docker Compose service names, DNS hostnames, volumes,
and Traefik labels to use the cameleer- prefix for clear ownership.

Services renamed:
- postgres → cameleer-postgres
- clickhouse → cameleer-clickhouse
- logto → cameleer-logto
- traefik → cameleer-traefik

Volumes renamed:
- pgdata → cameleer-pgdata
- chdata → cameleer-chdata
- certs → cameleer-certs
- bootstrapdata → cameleer-bootstrapdata

Updated across:
- docker-compose.yml, docker-compose.dev.yml
- installer/cameleer/docker-compose.yml
- installer/install.sh, installer/install.ps1
- application.yml defaults
- DockerTenantProvisioner.java hardcoded URL
- logto-bootstrap.sh defaults
- VendorTenantServiceTest.java
- CLAUDE.md, docs/architecture.md, docs/user-manual.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:51:33 +02:00
hsiegeln
fe6682e520 docs: update CLAUDE.md for deployment modes and admin merge
All checks were successful
CI / build (push) Successful in 1m59s
CI / docker (push) Successful in 23s
- Document standalone vs multi-tenant deployment modes
- Replace vendor references with SaaS admin
- Update bootstrap phases (Phase 12 always runs, no VENDOR_SEED_ENABLED)
- Update provisioning flow (no separate vendor user)
- Remove VENDOR_SEED_ENABLED from dev compose reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:04:57 +02:00
hsiegeln
012c866594 refactor: merge vendor user into saas-admin
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 17s
The admin user IS the platform admin — no separate vendor user needed.
The saas-vendor role is now always assigned to the admin user during
bootstrap. Removes VENDOR_ENABLED, VENDOR_USER, VENDOR_PASS from all
config, prompts, compose templates, and bootstrap script.

In multi-tenant mode: admin logs in with saas-admin credentials, gets
platform:admin scope via saas-vendor role, manages tenants directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:36:52 +02:00
hsiegeln
4e553a6c42 fix: add BOOTSTRAP_TOKEN to standalone server env
All checks were successful
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 15s
The cameleer3-server requires CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN
at startup. In standalone mode nothing uses it externally, but the
server's SecurityBeanConfig validates it exists. Generate a random
token in the .env and pass it through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:16:10 +02:00
hsiegeln
f254f2700f feat: standalone single-tenant deployment mode
All checks were successful
CI / build (push) Successful in 1m12s
CI / docker (push) Successful in 14s
Single-tenant installations now run the server directly without Logto
or the SaaS management plane. The installer generates a simpler compose
with 5 services: traefik, postgres, clickhouse, cameleer3-server, and
cameleer3-server-ui. Uses local auth (built-in admin), no OIDC.

Multi-tenant (vendor) mode is unchanged — full SaaS stack with Logto.

Changes:
- New DEPLOYMENT_MODE variable (standalone/saas) replaces TENANT_ORG_NAME
- generate_compose_file_standalone() for the 5-service compose
- Standalone traefik-dynamic.yml (no /platform/ redirect)
- Stock postgres:16-alpine (server creates schema via Flyway)
- Standalone health checks (server + UI instead of Logto + SaaS)
- Standalone credentials/docs generation
- Remove Phase 12b from bootstrap (no longer needed)
- Remove setup_single_tenant_record (no longer needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:12:02 +02:00
hsiegeln
17d8d98d5f fix: move single-tenant DB record creation from bootstrap to installer
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 17s
The bootstrap script runs before the SaaS app starts, but the tenants
table only exists after Flyway migrations run in the SaaS app. This
circular dependency caused Phase 12b's psql commands to fail under
set -e, crashing the Logto container on first install in single-tenant
mode.

Now the bootstrap only handles Logto-side setup (org, user roles, OIDC
redirect URIs), and the installer creates the tenant DB record after
verify_health confirms the SaaS app is up. Also makes docker_compose_up
tolerant of transient startup errors since verify_health is the real
health gate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:31:23 +02:00
hsiegeln
bfb26d9aa5 fix: guard logto entrypoint kill with || true to prevent set -e exit
All checks were successful
CI / build (push) Successful in 1m12s
CI / docker (push) Successful in 17s
When the background Logto process exits during bootstrap, `kill $LOGTO_PID`
returns non-zero. Under `set -e`, this terminates the entrypoint before
reaching the production-mode restart, causing the container to error on
first startup and only recover via restart policy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:12:22 +02:00
hsiegeln
cd4266ffc6 chore: remove redundant DOCKER_HOST env var from SaaS service
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 13s
TenantProvisionerAutoConfig already hardcodes the socket path via
.withDockerHost("unix:///var/run/docker.sock"). The env var was
redundant and not read by the Java Docker client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:02:42 +02:00
hsiegeln
74a1e02cb8 fix: replace env_file with explicit env vars for cameleer-saas
Some checks failed
CI / build (push) Failing after 2s
CI / docker (push) Has been skipped
Revert env_file approach — only pass the specific env vars the SaaS
app needs for its own database, identity, and tenant provisioning.
Organized into clear groups: Docker, SaaS database, Identity, and
Provisioning (passed to per-tenant servers).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:01:26 +02:00
hsiegeln
b3a19098c5 fix: pass all .env vars to cameleer-saas via env_file
Some checks failed
CI / build (push) Failing after 11s
CI / docker (push) Has been skipped
Instead of explicitly listing every env var the SaaS container needs,
use env_file to pass the entire .env. This ensures all installer-
configured values (passwords, hosts, ports, etc.) are available for
current and future use by the SaaS app and its provisioning config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:58:04 +02:00
hsiegeln
6b1dcba876 fix: pass ClickHouse password to SaaS provisioning config
All checks were successful
CI / build (push) Successful in 1m30s
CI / docker (push) Successful in 39s
The CLICKHOUSE_PASSWORD env var was set on the clickhouse container
but not passed to cameleer-saas. The provisioning properties defaulted
to 'cameleer_ch' instead of the installer-generated password, causing
tenant servers to fail ClickHouse authentication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:55:36 +02:00
hsiegeln
38125f9ecc fix: update tests for new ProvisioningProperties constructor args
All checks were successful
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 41s
Add datasourceUsername and datasourcePassword to test constructors
to match the updated record definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:48:35 +02:00
hsiegeln
6b95cf78ea fix: add datasource username/password defaults to application.yml
Some checks failed
CI / build (push) Failing after 37s
CI / docker (push) Has been skipped
The new ProvisioningProperties record fields need defaults in
application.yml or Spring Boot fails to bind the configuration.
Defaults to POSTGRES_USER/POSTGRES_PASSWORD env vars with
fallback to cameleer/cameleer_dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:46:38 +02:00
hsiegeln
b70d95cbb9 fix: pass database credentials to per-tenant servers via config
Some checks failed
CI / build (push) Failing after 38s
CI / docker (push) Has been skipped
The DockerTenantProvisioner hardcoded SPRING_DATASOURCE_USERNAME
and SPRING_DATASOURCE_PASSWORD as "cameleer" / "cameleer_dev".
With the installer generating random passwords, tenant servers
failed to connect to PostgreSQL.

Add datasourceUsername and datasourcePassword to ProvisioningProperties,
pass them from the compose env vars, and use them in the provisioner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:44:32 +02:00
hsiegeln
8b9045b0e2 fix: detect Docker socket GID for container permissions
All checks were successful
CI / build (push) Successful in 1m13s
CI / docker (push) Successful in 12s
The Docker socket group varies by host (e.g., GID 1001 on WSL2).
Hardcoding group_add: ["0"] doesn't work when the socket is owned
by a different group. The installer now detects the socket GID at
install time via stat. The main docker-compose.yml uses a
configurable DOCKER_GID env var (defaults to 0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:39:20 +02:00
hsiegeln
4fe642b91d fix: add Docker socket mount and DOCKER_HOST to SaaS service
All checks were successful
CI / build (push) Successful in 1m12s
CI / docker (push) Successful in 15s
The cameleer-saas service needs Docker socket access for tenant
provisioning. Add the socket bind mount, group_add for permissions,
and explicit DOCKER_HOST=unix:///var/run/docker.sock to prevent
the Java Docker client from falling back to TCP (which happens on
WSL2 + Docker Desktop when DOCKER_HOST leaks from the host env).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:30:55 +02:00
hsiegeln
7e13b4ee5d fix(installer): use Docker health status instead of exec for verification
All checks were successful
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 16s
Replace docker compose exec commands with Docker's built-in health
status checks. The exec-based ClickHouse check hung because
clickhouse-client waits for the server during initialization.
Docker's healthcheck status is already configured in compose and
is more reliable. Logto + Bootstrap merged into one check since
the healthcheck includes the bootstrap.json file test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:28:04 +02:00
hsiegeln
85eabd86ef feat: add deployment mode — vendor (multi-tenant) or single-tenant
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 17s
Installer now asks deployment mode in simple mode:
- Multi-tenant vendor: creates saas-vendor role + assigns to admin
- Single tenant: asks for org name, creates Logto org + tenant record,
  assigns admin as org owner

Reverts always-create-vendor-role — role is only created when vendor
mode is selected. TENANT_ORG_NAME env var passed to bootstrap for
single-tenant org creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:18:25 +02:00
hsiegeln
b44f6338f8 fix: always assign saas-vendor role to admin user
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 13s
The admin user needs platform:admin to create tenants via the vendor
console. Previously the saas-vendor role was only created when
VENDOR_SEED_ENABLED=true (for a separate vendor user). Now the role
is always created and assigned to the admin user. VENDOR_SEED_ENABLED
only controls creating the separate vendor user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:09:10 +02:00
hsiegeln
4ff04c386e fix(installer): force lowercase hostname in merge_config
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 15s
Moves hostname normalization into merge_config() so it applies
regardless of source (CLI flag, env var, config file, prompt,
auto-detect). Logto normalizes hostnames internally — case mismatch
causes JWT issuer validation failure (401).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:04:30 +02:00
hsiegeln
b38f02eae3 fix(installer): fix ClickHouse health check and normalize hostname
All checks were successful
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 19s
- ClickHouse health check: use $CLICKHOUSE_PASSWORD directly instead
  of extracting from .env via grep (nested quoting broke in eval)
- Normalize auto-detected hostname to lowercase (Windows returns
  uppercase which causes OIDC issuer case mismatches)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:58:32 +02:00
hsiegeln
8c504b714d fix: use BOOTSTRAP_LOCAL flag to skip Host headers in bootstrap
All checks were successful
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 16s
When running inside the Logto container (BOOTSTRAP_LOCAL=true), the
bootstrap script skips Host and X-Forwarded-Proto headers on all curl
calls. This avoids issuer mismatches when Logto runs with localhost
endpoints during bootstrap mode. PUBLIC_HOST/PUBLIC_PROTOCOL remain
unchanged so redirect URIs are generated with the correct public values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:44:02 +02:00
hsiegeln
83801d2499 fix: use localhost for bootstrap, restart Logto with public endpoints
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 13s
Start Logto with localhost endpoints so bootstrap can reach the
Management API without going through Traefik. After bootstrap
completes, restart Logto with the real public endpoints for
production use. This eliminates the Traefik race condition entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:28:19 +02:00
hsiegeln
9042356e81 fix: wait for Traefik to discover routes before bootstrap
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 16s
The Management API requires the admin OIDC endpoint (ADMIN_ENDPOINT)
to be reachable. Since bootstrap now runs inside the Logto container
(not a separate container), Traefik may not have discovered the labels
yet. Wait for the admin endpoint to be routable before running bootstrap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:22:05 +02:00
hsiegeln
f97e951d87 fix: add db alteration deploy step to Logto entrypoint
All checks were successful
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 14s
Newer Logto versions require `npm run cli db alteration deploy` after
seeding to apply schema migrations. Without this, Logto fails with
"relation systems does not exist".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:12:06 +02:00
hsiegeln
fa6bca0add fix: use apk instead of apt-get in Logto Dockerfile
All checks were successful
CI / build (push) Successful in 1m7s
CI / docker (push) Successful in 1m12s
The Logto base image (ghcr.io/logto-io/logto:latest) is Alpine-based,
not Debian. Switch from apt-get to apk for installing bootstrap deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:47:05 +02:00
hsiegeln
11dd6a354f feat(installer): add PowerShell installer for Windows
Some checks failed
CI / build (push) Successful in 1m24s
CI / docker (push) Failing after 25s
Mirrors install.sh structure and produces identical output files.
Uses native PowerShell idioms for parameters, prompts, and crypto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:39:24 +02:00
hsiegeln
7f15177310 feat(installer): add main function and complete install.sh
Appends the main() entry point that wires together all installer phases:
arg parsing, config loading, rerun detection, prerequisites, auto-detect,
interactive prompts, config merge/validate, password generation, file
generation, docker pull/up, health verification, and output printing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:33:15 +02:00
hsiegeln
b01f6e5109 feat(installer): add re-run, upgrade, and reinstall logic 2026-04-13 16:32:02 +02:00
hsiegeln
8146f072df feat(installer): add output file generation (credentials, INSTALL.md, config) 2026-04-13 16:31:38 +02:00
hsiegeln
f13fd3faf0 feat(installer): add docker operations and health verification 2026-04-13 16:30:53 +02:00
hsiegeln
5e5bc97bf5 feat(installer): add .env and docker-compose.yml generation 2026-04-13 16:30:32 +02:00
hsiegeln
7fc80cad58 feat(installer): add config merge, validation, and password generation 2026-04-13 16:25:34 +02:00