fix: Cloudron selectors, docker-compose env_file, CLOUDRON_BASE URL

- Fix Cloudron invite form selectors to match real page IDs
  (#inputUsername, #inputDisplayName, #inputPassword, #inputPasswordRepeat)
- Fix docker-compose.yml: use env_file instead of ${VAR} interpolation
  (password contains $ chars that docker-compose corrupts)
- Update CLOUDRON_BASE from tsys-cloudron.knel.net to my.knownelement.com

Phase 1 Cloudron enrollment now works end-to-end: invite accepted,
password set, TOTP extracted, credential stored in Bitwarden.
Phase 2 (Gitea/Redmine/Discourse) needs selector updates for
the current UI versions of each system.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
TSYS Group COO
2026-08-13 13:20:43 -05:00
parent 8c90d6809b
commit 3569a09afd
2 changed files with 15 additions and 24 deletions
+2 -7
View File
@@ -2,13 +2,8 @@ services:
provision:
build: .
container_name: tsys-agent-provisioner
environment:
- BW_CLIENTID=${BW_CLIENTID}
- BW_CLIENTSECRET=${BW_CLIENTSECRET}
- BW_PASSWORD=${BW_PASSWORD}
- BW_SERVER=${BW_SERVER:-https://pwvault.turnsys.com}
- BW_TOTP_SECRET=${BW_TOTP_SECRET:-}
- HEADFUL=${HEADFUL:-false}
env_file:
- ${BW_ENV_FILE:-${HOME}/.config/bw/env}
volumes:
- ./agents.yaml:/app/agents.yaml:ro
- ./state:/app/state
+13 -17
View File
@@ -44,7 +44,7 @@ log = logging.getLogger("provision")
# Constants
# ---------------------------------------------------------------------------
CLOUDRON_BASE = os.environ.get("CLOUDRON_BASE", "https://tsys-cloudron.knel.net")
CLOUDRON_BASE = os.environ.get("CLOUDRON_BASE", "https://my.knownelement.com")
GITEA_URL = os.environ.get("GITEA_URL", "https://git.knownelement.com")
DISCOURSE_URL = os.environ.get("DISCOURSE_URL", "https://community.turnsys.com")
REDMINE_URL = os.environ.get("REDMINE_URL", "https://projects.knownelement.com")
@@ -88,29 +88,25 @@ def enroll_cloudron(
# Navigate to invite link
page.goto(invite_url, wait_until="networkidle")
page.wait_for_timeout(2000)
# Fill in the invite acceptance form
# Cloudron invite page typically has: username (may be pre-filled), password, confirm password
page.wait_for_selector('input[type="password"]', timeout=15000)
# Fill in the invite acceptance form using real Cloudron selectors
page.wait_for_selector('#inputPassword', timeout=15000)
password_inputs = page.query_selector_all('input[type="password"]')
if len(password_inputs) >= 2:
password_inputs[0].fill(password)
password_inputs[1].fill(password)
elif len(password_inputs) == 1:
password_inputs[0].fill(password)
else:
raise RuntimeError(f"[{name}] No password field found on Cloudron invite page")
page.fill('#inputPassword', password)
page.fill('#inputPasswordRepeat', password)
# Set display name if field exists
name_field = page.query_selector('input[name="displayName"], input[name="name"]')
if name_field:
name_field.fill(display_name)
display_field = page.query_selector('#inputDisplayName')
if display_field:
display_field.fill(display_name)
# Submit
submit = page.query_selector('button[type="submit"], button:has-text("Setup"), button:has-text("Create"), button:has-text("Accept")')
# Submit — Cloudron uses a button with class btn-primary or type submit
submit = page.query_selector('button[type="submit"], button.btn-primary, button:has-text("Setup"), button:has-text("Create"), button:has-text("Accept")')
if submit:
submit.click()
else:
page.keyboard.press('Enter')
page.wait_for_load_state("networkidle")
log.info(f"[{name}] Invite accepted")