feat(v3.13.0): settings live DB reads, remove Directus/Appwrite, admin stats

Settings fixes:
- MAX_SITES_PER_USER, USER_RATE_LIMIT_PER_MIN/HR now read from DB settings
  table (DB > ENV > default), so dashboard/settings changes apply without
  restart. Sync cache refreshed on every save or delete.
- /api/me reports the live DB value for max_sites_per_user.

Admin improvements:
- Admin users bypass per-user rate limiting entirely (role=admin or ADMIN_EMAILS).
- Admin Overview now shows platform stats: registered users, new users (7d),
  total user sites, available tools.

Plugin cleanup:
- Appwrite and Directus plugins removed from the active registry (8 plugins
  now: WordPress, WooCommerce, WordPress Specialist, Gitea, n8n, Supabase,
  OpenPanel, Coolify). Plugin code is retained for future re-enabling.
- Settings page plugin visibility list updated to match.

Mobile onboarding:
- Stepper steps on narrow viewports stack vertically with correct full border
  and rounded corners on each step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 23:33:20 +02:00
parent f203ca88de
commit 43fd2201a0
223 changed files with 36183 additions and 4115 deletions

View File

@@ -147,6 +147,109 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"scope": "write",
},
{
"name": "list_navigations",
"method_name": "list_navigations",
"description": (
"List wp_navigation posts (block-theme navigation menus). "
"These power the <!-- wp:navigation --> block in Site Editor. "
"Distinct from classic menus (use list_menus for those)."
),
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Results per page (1-100)",
"default": 20,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"status": {
"type": "string",
"description": "Status filter",
"enum": ["publish", "draft", "pending", "private", "any"],
"default": "any",
},
},
},
"scope": "read",
},
{
"name": "get_navigation",
"method_name": "get_navigation",
"description": (
"Get a single wp_navigation post by ID, including its raw "
"block-markup content. Use the returned content to plan an "
"update_navigation call (e.g. fix href values inside <!-- "
"wp:navigation-link --> blocks)."
),
"schema": {
"type": "object",
"properties": {
"navigation_id": {
"type": "integer",
"description": "wp_navigation post ID",
"minimum": 1,
},
},
"required": ["navigation_id"],
},
"scope": "read",
},
{
"name": "update_navigation",
"method_name": "update_navigation",
"description": (
"Update a wp_navigation post (block-theme menu). Writes "
"post_content (block markup) and/or title/status/slug/meta. "
"Use this to fix navigation-link hrefs that the agent set "
"up in Site Editor without leaving the MCP loop."
),
"schema": {
"type": "object",
"properties": {
"navigation_id": {
"type": "integer",
"description": "wp_navigation post ID",
"minimum": 1,
},
"title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Navigation title",
},
"content": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": (
"Block markup (e.g. <!-- wp:navigation-link "
'{"label":"About","url":"/about/"} /-->). '
"Replaces the entire post_content."
),
},
"status": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Publication status",
"enum": ["publish", "draft", "pending", "private"],
},
"slug": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Navigation slug",
},
"meta": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Post meta key/value map (registered keys only).",
},
},
"required": ["navigation_id"],
},
"scope": "write",
},
]
@@ -401,3 +504,123 @@ class MenusHandler:
{"error": str(e), "message": f"Failed to update menu item {item_id}: {str(e)}"},
indent=2,
)
async def list_navigations(self, per_page: int = 20, page: int = 1, status: str = "any") -> str:
"""List wp_navigation posts (block-theme menus)."""
try:
params = {"per_page": per_page, "page": page, "status": status}
items = await self.client.get("navigation", params=params)
if isinstance(items, list):
summary = [
{
"id": n.get("id"),
"title": (n.get("title") or {}).get("rendered", ""),
"slug": n.get("slug"),
"status": n.get("status"),
"modified": n.get("modified"),
}
for n in items
]
else:
summary = items
return json.dumps(
{"total": len(summary) if isinstance(summary, list) else 0, "navigations": summary},
indent=2,
)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list navigations: {str(e)}"}, indent=2
)
async def get_navigation(self, navigation_id: int) -> str:
"""Get a single wp_navigation post (with raw block content)."""
try:
# context=edit returns content.raw so the agent can round-trip it
params = {"context": "edit"}
nav = await self.client.get(f"navigation/{navigation_id}", params=params)
content = nav.get("content") or {}
raw_content = content.get("raw") if isinstance(content, dict) else None
rendered = content.get("rendered") if isinstance(content, dict) else None
return json.dumps(
{
"id": nav.get("id"),
"title": (nav.get("title") or {}).get("rendered", ""),
"slug": nav.get("slug"),
"status": nav.get("status"),
"content_raw": raw_content,
"content_rendered": rendered,
"modified": nav.get("modified"),
"link": nav.get("link"),
},
indent=2,
)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to get navigation {navigation_id}: {str(e)}",
},
indent=2,
)
async def update_navigation(
self,
navigation_id: int,
title: str | None = None,
content: str | None = None,
status: str | None = None,
slug: str | None = None,
meta: dict | None = None,
) -> str:
"""Update a wp_navigation post (block-theme menu)."""
try:
data: dict[str, Any] = {}
if title is not None:
data["title"] = title
if content is not None:
data["content"] = content
if status is not None:
data["status"] = status
if slug is not None:
data["slug"] = slug
if meta is not None:
data["meta"] = meta
if not data:
return json.dumps(
{
"error": "no_fields",
"message": (
"update_navigation requires at least one of "
"title, content, status, slug, meta."
),
},
indent=2,
)
nav = await self.client.post(f"navigation/{navigation_id}", json_data=data)
return json.dumps(
{
"id": nav.get("id"),
"title": (nav.get("title") or {}).get("rendered", ""),
"slug": nav.get("slug"),
"status": nav.get("status"),
"modified": nav.get("modified"),
"link": nav.get("link"),
"message": f"Navigation {navigation_id} updated successfully",
},
indent=2,
)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to update navigation {navigation_id}: {str(e)}",
},
indent=2,
)