feat: v3.4.0 — OpenPanel plugin public release (42 tools)

OpenPanel product analytics plugin fully reviewed, tested, and published.
Works with both self-hosted and cloud (openpanel.dev) instances.

- 42 tools: event tracking, data export, analytics, project/client management
- All tools use public REST APIs (Track, Export, Insights, Manage)
- Client modes: write (tracking), read (analytics), root (full access)
- Service page with description, setup notes, WordPress plugin download
- Dynamic URL hints in Add Site form
- 62 unit tests
- ENABLED_PLUGINS default now includes openpanel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 17:48:12 +02:00
parent 68c84b3ece
commit 9905a28eb0
33 changed files with 1672 additions and 3207 deletions

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)