Tests now mock tenantRepository.findById() since provisionAsync re-loads
the tenant entity, and assert on the entity directly rather than the
return value of createAndProvision().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vendor: POST /api/vendor/tenants/{id}/restart (platform:admin scope)
Tenant: POST /api/tenant/server/restart (tenant:manage scope)
Both call TenantProvisioner.stop() then start() on the server + UI
containers. Restart button on vendor TenantDetailPage (Actions card)
and tenant TenantDashboardPage (Server card). Allowed in any status
including PROVISIONING.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend: extract Docker provisioning into @Async method so the API
returns immediately with status=PROVISIONING. The tenant record, Logto
org, admin user, and license are created synchronously; container
provisioning, health check, license push, and OIDC config happen in a
background thread.
Frontend: navigate to tenant detail page immediately after creation.
Detail page polls every 3s while status=PROVISIONING and shows a
spinner indicator. Toast notification when provisioning completes.
Fixes#52.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add LogtoManagementClient methods for SSO connector CRUD + org JIT
- Add TenantSsoService with tenant isolation (validates connector-org link)
- Add TenantSsoController at /api/tenant/sso with test endpoint
- Create SsoPage with provider selection, dynamic config form, test button
- Remove old OIDC config endpoints from tenant portal (server OIDC is
now platform-managed, set during provisioning)
- Sidebar: OIDC -> SSO with Shield icon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now hardcodes Logto org scopes in the auth flow, so the
provisioner no longer needs to push them via OIDC config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace JPQL @Query with dynamic SQL via JdbcTemplate to avoid
Hibernate null parameter type issues (bytea vs text). Conditionally
appends WHERE clauses only for non-null filters, matching the proven
pattern from cameleer3-server's PostgresAuditRepository.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hibernate binds null String params as bytea, causing PostgreSQL
lower(bytea) error. Convert null search to empty string in service
layer, use empty-string check in JPQL instead of IS NULL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hibernate passes null search param as bytea type, causing PostgreSQL
to fail on LOWER(bytea). COALESCE converts null to empty string.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this, hard refresh on SPA routes returns 401 because Spring
Security intercepts before SpaController can forward to index.html.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vendor sees all audit events with tenant filter at /vendor/audit.
Tenant admin sees only their own events at /tenant/audit.
Both support pagination, action/result filters, and text search.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The server needs a shared Docker volume at /data/jars to store
uploaded JARs that deployed app containers can access. Without this
mount, deployed containers fail with "Unable to access jarfile".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without urn:logto:scope:organizations and
urn:logto:scope:organization_roles in the additionalScopes, Logto
doesn't include organization role data in the Custom JWT context.
This caused the roles claim to be empty, so all OIDC users got
defaultRoles (VIEWER) instead of their org role (e.g. owner →
server:admin).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Traefik's Docker provider resolves container IPs using the configured
default network ('cameleer'). For dynamically-created containers not
managed by compose, this network name doesn't match. Adding the
traefik.docker.network label explicitly tells Traefik to use the
cameleer-traefik network for routing, fixing 504 Gateway Timeouts
on /t/{slug}/api/* paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The nginx template in cameleer3-server-ui uses ${CAMELEER_API_URL} for
the upstream proxy target, not API_URL. The wrong env var name caused
the baked-in default (http://cameleer3-server:8081) to be used, which
doesn't resolve in per-tenant networks where the server is named
cameleer-server-{slug}.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After provisioning a server, pushes Logto Traditional Web App
credentials (client ID + secret) via the server's OIDC admin API.
This enables SSO: users authenticated via Logto can access the
server dashboard without a separate login.
Reads tradAppSecret from bootstrap JSON via LogtoConfig.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sidebar: Tenants moved into expandable "Vendor" section with
sub-items for Tenants and Identity (Logto console link)
- Bootstrap: removed example organization creation (Phase 6 org)
— tenants are now created exclusively via the vendor console
- Removed BootstrapDataSeeder (no auto-seeded tenant/license)
- Bootstrap log updated to reflect clean-slate approach
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each tenant gets an isolated Docker bridge network (cameleer-tenant-{slug}).
Server + UI containers use the tenant network as primary, with additional
connections to the shared services network (postgres/clickhouse/logto) and
Traefik network (routing). Tenant networks are internal (no internet) and
isolated from each other. Apps deployed by the tenant server also join
the tenant network. Network is removed on tenant delete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During tenant provisioning, adds /t/{slug}/oidc/callback to the Logto
Traditional Web App's registered redirect URIs. This enables the
server's OIDC login flow to work when accessed via Traefik routing.
Also reads tradAppId from bootstrap JSON via LogtoConfig.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When creating a tenant, the vendor can specify adminUsername +
adminPassword. The backend creates the user in Logto and assigns them
the owner org role. The vendor user is also auto-added to every new
org for support access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
existsBySlug found DELETED records, blocking slug reuse. Changed to
existsBySlugAndStatusNot(slug, DELETED) so deleted tenant slugs can
be reclaimed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The server's /actuator/health requires auth. The public health endpoint
is /api/v1/health (same as compose-managed server's Docker HEALTHCHECK).
Also increased health check retries/timeout and added startPeriod.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Traefik strip-prefix middleware so /t/{slug}/api -> /api on server
- Add priority to routers (server API=10, UI=5) to prevent conflicts
- Mount Docker socket + group_add in server containers for app deployment
- Add JAR storage, Docker network, volume env vars for runtime orchestrator
- Use HashMap for labels (>10 entries exceeds Map.of limit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds CAMELEER_AUTH_TOKEN, CAMELEER_JWT_SECRET, CAMELEER_OIDC_AUDIENCE,
CLICKHOUSE_URL to provisioned server containers. Also passes PUBLIC_HOST
and PUBLIC_PROTOCOL to SaaS container in dev overlay so provisioner
resolves the correct hostname instead of defaulting to localhost.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Splits the flat 3-page UI into /vendor/* (platform:admin) and /tenant/*
(all authenticated users) route trees, with stub pages, new API hooks,
updated Layout with persona-aware sidebar, and SpaController forwarding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace stub with full Docker implementation using docker-java. Manages
per-tenant server and UI containers with Traefik labels, health checks,
image pull, network attachment, and full lifecycle (provision/start/stop/remove/status).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds server_endpoint and provision_error columns to tenants table (V011 migration),
updates TenantEntity and TenantResponse with new fields and a from() factory,
adds revokeLicense() to LicenseService, and updates TenantController to use the factory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- V010: drop deployments, apps, environments, api_keys tables
- Tables have been migrated to cameleer3-server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete environment/, app/, deployment/, runtime/ packages (source + tests)
- Delete apikey/ package (tied to environments, table will be dropped)
- Strip AsyncConfig to empty @EnableAsync (no more deploymentExecutor bean)
- Remove EnvironmentService dependency from TenantService
- Remove environment/app isolation from TenantIsolationInterceptor
- Remove environment seeding from BootstrapDataSeeder
- Refactor ServerApiClient to use LogtoConfig instead of RuntimeConfig
- Add server-endpoint property to LogtoConfig (was in RuntimeConfig)
- Remove runtime config section and multipart config from application.yml
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ServerApiClient: use X-Cameleer-Protocol-Version: 1 (server expects "1", not "2")
- Disable Hibernate show-sql in dev profile (too verbose)
- CLAUDE.md: document deployment pipeline architecture, M2M server role in bootstrap,
runtime-base image in CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The default cameleer-runtime-base:latest has no registry prefix, so
Docker can't pull it. Use the full gitea.siegeln.net/cameleer/ path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The @Bean named 'deploymentExecutor' (ThreadPoolTaskExecutor) collided
with the @Service class DeploymentExecutor. Rename the bean to
'deploymentTaskExecutor'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The serverApiClient.isAvailable() stubbing in setUp() is unused by
the observability test, causing UnnecessaryStubbingException in CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Docker socket security: remove root group from Dockerfile, use
group_add in docker-compose.yml for runtime-only socket access
2. M2M server communication: create ServerApiClient using Logto
client_credentials grant with API resource scope. Add M2M server
role in bootstrap. Replace hacky admin/admin login in
AgentStatusService.
3. Async deployment: extract DeploymentExecutor as separate @Service
so Spring's @Async proxy works (self-invocation bypasses proxy).
Deploy now returns immediately, health check runs in background.
4. Bootstrap: M2M server role (cameleer-m2m-server) with server:admin
scope, idempotent creation outside the M2M app creation block.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for the deployment pipeline:
1. Health check path: /health -> /cameleer/health (matches agent)
2. Container cleanup: stop AND remove old container before starting
new one, plus orphan cleanup by container name to prevent conflicts
3. Container status: read health.status instead of state.status so
waitForHealthy correctly detects the "healthy" state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The httpclient5 transport needs junixsocket for Unix domain sockets.
Switch to docker-java-transport-zerodep which has built-in Unix socket
support with zero external dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Default docker-java config resolved to localhost:2375 (TCP) inside the
container. Explicitly set docker host to unix:///var/run/docker.sock
which is volume-mounted from the host.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CI step to build cameleer-runtime-base image by downloading the
agent shaded JAR from Gitea Maven registry and pushing the image.
Wire CAMELEER_AUTH_TOKEN from docker-compose into RuntimeConfig so
deployed containers authenticate with cameleer3-server. Add agent.jar
to gitignore for local builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spring Boot defaults to 1MB max file size which rejected all JAR
uploads. Set to 200MB to match the configured max-jar-size. Also
create /data/jars with cameleer user ownership in the Dockerfile
so the non-root process can write uploaded JARs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add per-app memory limit and CPU shares (stored on AppEntity, used by
DeploymentService with fallback to global defaults). JAR upload is now
optional at creation time. Both create modals show the computed slug in
the dialog title and use consistent Cancel-left/Action-right button
layout with inline styles to avoid Modal CSS conflicts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sidebar, sign-in page, and favicons all use the single SVG
- Postinstall copies SVG for SaaS HTML favicon (gitignored)
- Sign-in favicon committed (baked into Logto Docker image)
- Remove old PNG favicon references
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>