chore(F.4e): remove env-based site loading, unify OAuth Clients, update docs

- Remove env-based site discovery from SiteManager and ProjectManager
- Unify admin and user OAuth Clients into single role-based page
- Update all documentation to reflect DB-based site management
- Clean up env.example, README, ARCHITECTURE docs
- Improve Projects page empty state with link to My Sites
- Remove obsolete env discovery tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 00:24:33 +02:00
parent 81417b552f
commit 3815f5a6d5
23 changed files with 202 additions and 782 deletions

View File

@@ -4,8 +4,8 @@ Uses REST APIs:
- Export API (GET /export/events, /export/charts) for raw data export
- Insights API (GET /insights/:projectId/*) for analytics queries
Note: project_id is optional if configured in environment variables.
When not provided, the default project_id from OPENPANEL_SITE1_PROJECT_ID is used.
Note: project_id is optional if configured in the site settings.
When not provided, the default project_id from the site configuration is used.
Requires 'read' or 'root' mode client for Export API.
"""

View File

@@ -23,6 +23,6 @@ def get_project_id(client: OpenPanelClient, project_id: str | None) -> str:
return client.default_project_id
raise ValueError(
"project_id is required. Either provide it as a parameter or configure "
"OPENPANEL_SITE1_PROJECT_ID in environment variables. "
"the project_id field when adding the site in the dashboard. "
"You can find your Project ID in OpenPanel Dashboard → Project Settings."
)

View File

@@ -483,6 +483,8 @@ async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: st
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
async def search_users(
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
) -> str:

View File

@@ -67,18 +67,17 @@ class WordPressClient:
# Validate required parameters
if not site_url:
raise ConfigurationError(
"Site URL is not configured. "
"Please set the URL environment variable (e.g., WORDPRESS_SITE1_URL)."
"Site URL is not configured. " "Please add or update the site in the dashboard."
)
if not username:
raise ConfigurationError(
"Username is not configured. "
"Please set the USERNAME environment variable (e.g., WORDPRESS_SITE1_USERNAME)."
"Please update the site credentials in the dashboard."
)
if not app_password:
raise ConfigurationError(
"App password is not configured. "
"Please set the APP_PASSWORD environment variable (e.g., WORDPRESS_SITE1_APP_PASSWORD)."
"Please update the site credentials in the dashboard."
)
self.site_url = site_url.rstrip("/")

View File

@@ -77,12 +77,8 @@ class WPCLIManager:
try:
# First, test if we have Docker socket access
test_process = await asyncio.create_subprocess_exec(
"docker",
"version",
"--format",
"{{.Server.Version}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "version", "--format", "{{.Server.Version}}",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
test_stdout, test_stderr = await asyncio.wait_for(
@@ -103,15 +99,10 @@ class WPCLIManager:
# Now check for our specific container using exact name match
process = await asyncio.create_subprocess_exec(
"docker",
"ps",
"--all",
"--filter",
f"name=^{self.container_name}$",
"--format",
"{{.Names}}|{{.Status}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "ps", "--all",
"--filter", f"name=^{self.container_name}$",
"--format", "{{.Names}}|{{.Status}}",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
@@ -126,13 +117,8 @@ class WPCLIManager:
if not output:
# Container not found - get list of available containers for helpful error
list_process = await asyncio.create_subprocess_exec(
"docker",
"ps",
"--all",
"--format",
"{{.Names}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "ps", "--all", "--format", "{{.Names}}",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
list_stdout, _ = await list_process.communicate()
available = list_stdout.decode().strip().split("\n") if list_stdout else []
@@ -184,14 +170,9 @@ class WPCLIManager:
try:
# Try to run wp --version
process = await asyncio.create_subprocess_exec(
"docker",
"exec",
self.container_name,
"wp",
"--version",
"--allow-root",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "exec", self.container_name,
"wp", "--version", "--allow-root",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
@@ -310,9 +291,7 @@ class WPCLIManager:
)
# 4. Build docker exec command (split command into args to prevent shell injection)
cmd_parts = (
["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"]
)
cmd_parts = ["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"]
self.logger.info(f"Executing: {' '.join(cmd_parts)}")
@@ -603,29 +582,17 @@ class WPCLIManager:
try:
# Try GNU stat first, fall back to BSD stat
process = await asyncio.create_subprocess_exec(
"docker",
"exec",
self.container_name,
"stat",
"-c",
"%s",
export_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "exec", self.container_name,
"stat", "-c", "%s", export_path,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
if process.returncode != 0:
# BSD stat fallback
process = await asyncio.create_subprocess_exec(
"docker",
"exec",
self.container_name,
"stat",
"-f",
"%z",
export_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
"docker", "exec", self.container_name,
"stat", "-f", "%z", export_path,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)

View File

@@ -67,7 +67,7 @@ class WordPressAdvancedPlugin(BasePlugin):
if not container_name:
raise ValueError(
"WordPress Advanced plugin requires 'container' configuration. "
"Please set WORDPRESS_ADVANCED_SITE1_CONTAINER in environment variables."
"Please set the 'container' field when adding the site in the dashboard."
)
# Import WP-CLI manager