Add 32px card padding and reduce max-width to 420px for consistency
with the sign-in page. Add noValidate to prevent browser-native
required validation outlines on inputs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Newer Logto versions redirect to /register?app_id=... instead of
/sign-in?first_screen=register. Check the pathname in addition to
the query param so the registration form shows correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These routes were missing from SpaController, so requests to
/platform/register and /platform/onboarding had no handler. Spring
forwarded to /error, which isn't in the permitAll() list, resulting
in a 401 instead of serving the SPA.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show "Please enter a valid email address" when the user enters a
username instead of an email in the sign-up form, rather than letting
it hit Logto's API and returning a cryptic 400.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto validates M2M tokens by fetching its own JWKS from the ENDPOINT
URL (e.g. https://app.cameleer.io/oidc/jwks). Behind a Cloudflare
tunnel, that hostname resolves to Cloudflare's IP and the container
can't route back through the tunnel — the fetch times out (ETIMEDOUT),
causing all Management API calls to return 500.
Adding extra_hosts maps AUTH_HOST to host-gateway so the request goes
to the Docker host, which has Traefik on :443, which routes back to
Logto internally. This hairpin works because NODE_TLS_REJECT=0 accepts
the self-signed cert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upgrade path in handle_rerun called load_config_file a second time
(already called by detect_existing_install). On the second pass, every
variable is already set, so [ -z "$VAR" ] && VAR="$value" returns
exit code 1 (test fails, && short-circuits). With set -e, the non-zero
exit from the case clause kills the script silently after printing
"[INFO] Upgrading installation..." — no error, no further output.
Removed the redundant load_config_file and load_env_overrides calls.
Both were already executed in main() before handle_rerun is reached.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Traefik auto-calculates router priority from rule string length. When
deployed with a domain longer than 23 chars (e.g. app.cameleer.io),
Host(`app.cameleer.io`) (25 chars) outranks PathPrefix(`/platform`)
(23 chars), causing ALL requests — including /platform/* — to route
to Logto instead of the SaaS app. This breaks login because the sign-in
UI loads without an OIDC interaction session.
Setting priority=1 makes Logto a true catch-all, matching the intent
documented in docker/CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The root redirect rule matches Host(`PUBLIC_HOST`), not localhost.
Curl with --resolve (bash) and Host header (PS1) so the health
check sends the right hostname when verifying Traefik routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both simple and expert modes now ask "Pull images from a private
registry?" with follow-up prompts for URL, username, and token.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both installers (bash + PS1) now support pulling images from a
custom Docker registry. Writes *_IMAGE env vars to .env so compose
templates use the configured registry. Runs docker login before
pull when credentials are provided. Persisted in cameleer.conf
for upgrades/reconfigure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`tr | head -c 32` causes tr to receive SIGPIPE when head exits early.
With `set -eo pipefail`, exit code 141 kills the script right after
"Configuration validated" before any passwords are generated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The docker-builder container runs ash/sh, not bash — arrays with ()
are not supported. Use a simple for-in loop instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssh-keyscan fails when the runner can't reach the host on port 22
during that step. Using accept-new on the ssh command itself is
equivalent for an ephemeral CI runner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pulls all :latest images from the Gitea registry and pipes them
via `docker save | ssh docker load` to the APP_HOST server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support separate auth domain (e.g. auth.cameleer.io) for Logto while
keeping the SaaS app on PUBLIC_HOST (e.g. app.cameleer.io). AUTH_HOST
defaults to PUBLIC_HOST for backward-compatible single-domain setups.
- Logto routing: Host(AUTH_HOST) replaces PathPrefix('/') catch-all
- Root redirect moved from traefik-dynamic.yml to Docker labels with
Host(PUBLIC_HOST) scope so it doesn't intercept auth domain
- Self-signed cert generates SANs for both domains
- Bootstrap Host header uses AUTH_HOST for Logto endpoint validation
- Spring issuer-uri and oidcissueruri use new authhost property
- Both installers (sh + ps1) prompt for AUTH_HOST in expert mode
Local dev: AUTH_HOST=auth.localhost (resolves to 127.0.0.1, no hosts file)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fleet overview page at /vendor/metrics showing per-tenant operational
metrics (agents, CPU, heap, HTTP requests, ingestion drops, uptime).
Queries each tenant's server via the new POST /api/v1/admin/server-metrics/query
REST API instead of direct ClickHouse access, supporting future per-tenant
CH instances.
Backend: TenantMetricsService fires 11 metric queries per tenant
concurrently over a 5-minute window, assembles into a summary snapshot.
ServerApiClient.queryServerMetrics() handles the M2M authenticated POST.
Frontend: VendorMetricsPage with KPI strip (fleet totals) and per-tenant
table with color-coded badges and heap usage bars. Auto-refreshes every 60s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-fix the paragraph claimed every dynamically-created container MUST
carry `traefik.docker.network=cameleer-traefik` to avoid a 504, because
Traefik's Docker provider pointed at `network: cameleer` (a literal
name that never matched any real network). After the one-line static
config fix (df64573), Traefik's provider targets `cameleer-traefik`
directly — the network every managed container already joins — so the
per-container label is just defense-in-depth, not required.
Rewritten to describe current behaviour and keep a short note about the
pre-fix 504 for operators who roll back to an old image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The static config set `provider.docker.network: cameleer`, but no network
by that literal name exists. The `cameleer` network defined in the
compose file gets namespaced by compose to `cameleer_cameleer`, and
managed app containers created at runtime only ever attach to
`cameleer-traefik` (per `DockerNetworkManager.TRAEFIK_NETWORK`).
Symptom: when the Docker provider's preferred network doesn't match any
network on a container, Traefik picks an arbitrary container IP and may
route to one on a bridge Traefik itself isn't attached to — requests
hang until Traefik's upstream timeout fires (504 Gateway Timeout).
Fix is one line: match the network that `cameleer-server` actually
attaches its managed containers to.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server now requires a non-empty JWT secret. The installer (bash + ps1)
generates a random value for both SaaS and standalone modes, and the compose
templates map it into the respective containers. Also fixes container names
in generated INSTALL.md docs to use the cameleer- prefix consistently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The compose volume `jars` gets created as `<project>_jars` by Docker
Compose, but JARDOCKERVOLUME tells the server to mount `cameleer-jars`
on deployed app containers. These are different Docker volumes, so
the app JAR was never visible inside the app container — causing
ClassNotFoundException on startup.
Fix: add `name: cameleer-jars` to the volume definition so both the
server and deployed app containers share the same named volume.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The @Lazy self-proxy pattern requires a non-null reference in tests.
Construct the instance then re-create with itself as the self param.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add missing runtimeBaseImage arg to ProvisioningProperties constructor
calls in tests. Also add missing self-proxy arg to VendorTenantService
constructor (pre-existing from async provisioning commit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CAMELEER_SERVER_RUNTIME_BASEIMAGE was never set on provisioned
per-tenant server containers, causing them to fall back to the
server's hardcoded default. Added CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE
as a configurable property that gets forwarded during provisioning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Async on provisionAsync() was bypassed because all call sites were
internal (this.provisionAsync), skipping the Spring proxy. Inject self
via @Lazy to route through the proxy so provisioning runs in a
background thread and the API returns immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows Docker Compose uses ; not : as the path separator in COMPOSE_FILE.
The colon was being interpreted as part of the filename, causing CreateFile errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline heredoc compose generation with static template files.
Templates are copied to the install dir and composed via COMPOSE_FILE in .env.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reference .env file documenting all configuration variables across both
deployment modes, with section headers for compose assembly, public access,
credentials, TLS, Docker, provisioning, and monitoring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Optional compose overlays: TLS overlay mounts user-supplied certs into
traefik, monitoring overlay replaces the noop bridge with an external
Docker network for Prometheus scraping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone mode: server + server-ui services with postgres image override
to stock postgres:16-alpine. Includes traefik-dynamic.yml for default TLS
certificate store configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto identity provider and cameleer-saas management plane services.
Includes Traefik labels, CORS config, bootstrap healthcheck, and all
provisioning env vars parameterized from .env.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker compose templates defaulted to admin/admin when .env was missing.
Now uses :? to fail with a clear error instead of silently using weak creds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename Java packages from net.siegeln.cameleer3 to net.siegeln.cameleer,
update all references in workflows, Docker configs, docs, and bootstrap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The log appender JAR was missing from the cameleer-runtime-base Docker
image, causing agent log forwarding to silently fail with "No supported
logging framework found, log forwarding disabled". This meant only
container stdout logs (source=container) were captured — no application
or agent logs reached ClickHouse.
CI now downloads the appender JAR from the Maven registry alongside the
agent JAR, and the Dockerfile COPYs it to /app/cameleer3-log-appender.jar
where the server's Docker entrypoint expects it (-Dloader.path for
Spring Boot, -cp for plain Java).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Installers now use `--pull always --force-recreate` on `docker compose up`
to ensure fresh images are used on every install/reinstall, preventing
stale containers from missing schema changes like db_password.
Fix VendorTenantServiceTest to expect two repository saves in provisioning
tests (one for dbPassword, one for final status).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>