feat(F.7+F.17): v3.11.0 — Coolify plugin (67 tools) + tool access overhaul
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled

Catch-up sync spanning v3.7.0 → v3.11.0 of the internal repo.

Platform
- Total tools: 565 → 633 (+68) across 10 plugins (Coolify added)
- Tests: 481 → 828 passing

New plugin: Coolify (67 tools, Track F.17)
- Applications (17): CRUD, lifecycle, logs, env vars
- Deployments (5): list/get/cancel/deploy, app history
- Servers (8): CRUD, resources, domains, validation
- Projects (8), Databases (16, 6 DB types + backups), Services (13)

Tool access system (Track F.7 → F.7d)
- Scope → category mapping with per-tool `category` + `sensitivity`
- Schema v7: `site_tool_toggles(site_id)` + `sites.tool_scope` column
- Schema v8: per-site API keys (`api_keys.site_id`)
- Plugin-specific access-level presets (WP / WC / Gitea / OpenPanel /
  Coolify 5-tier)
- Credential-requirement notice tailored per plugin and tier
- Admin Tools count card on service page
- Dropped redundant `write` tier on WP / WP Advanced / WooCommerce
  (admin-scope tool count = 0 → identical to admin tier)

Dashboard
- Unified site manage page (Connection / Tool Access / Connect)
- /dashboard/keys unified (was /api-keys and /connect)
- CSRF interceptor via meta-tag; removed conflicting cookie reader
- Tailwind: pre-built CSS (scripts/build-css.sh) replaces CDN

Docs
- README / DOCKER_README / CLAUDE updated to 633 tools / 10 plugins
- CHANGELOG entries for v3.7.0 → v3.11.0
- FastMCP compatibility note updated to 3.x (post-v3.5 upgrade)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 21:05:07 +02:00
parent d8a0805412
commit 788439e377
31 changed files with 1911 additions and 254 deletions

View File

@@ -2913,16 +2913,22 @@ async def dashboard_connect_page(request: Request) -> Response:
async def dashboard_sites_view(request: Request) -> Response:
"""GET /dashboard/sites/{id}Show site connect page with config snippets."""
"""GET /dashboard/sites/{id}Unified site management page (F.7c).
Combines connection settings, tool access, and connect/config snippets
into a single page with 3 sections.
"""
user_session, redirect = _require_user_session(request)
if redirect:
return redirect
site_id = request.path_params.get("id", "")
import json
from core.config_snippets import get_supported_clients
from core.site_api import PLUGIN_DISPLAY_NAMES as SITE_PLUGIN_NAMES
from core.site_api import get_user_site
from core.site_api import get_user_credential_fields, get_user_plugin_names, get_user_site
from core.tool_access import get_scope_presets_for_plugin
site = await get_user_site(site_id, user_session["user_id"])
if site is None:
@@ -2936,15 +2942,23 @@ async def dashboard_sites_view(request: Request) -> Response:
public_url = os.environ.get("PUBLIC_URL", "http://localhost:8000").rstrip("/")
mcp_url = f"{public_url}/u/{user_session['user_id']}/{site['alias']}/mcp"
plugin_fields = get_user_credential_fields()
plugin_names = get_user_plugin_names()
scope_presets = get_scope_presets_for_plugin(site["plugin_type"])
return templates.TemplateResponse(
request,
"dashboard/sites/view.html",
"dashboard/sites/manage.html",
{
"lang": lang,
"t": t,
"session": user_session,
"site": site,
"plugin_names": SITE_PLUGIN_NAMES,
"plugin_names": plugin_names,
"plugin_fields": plugin_fields,
"plugin_fields_json": json.dumps(plugin_fields),
"scope_presets": scope_presets,
"scope_presets_json": json.dumps(scope_presets),
"mcp_url": mcp_url,
"clients": get_supported_clients(),
"current_page": "my_sites",
@@ -3009,7 +3023,9 @@ async def dashboard_keys_unified(request: Request) -> Response:
)
if user_session:
# User view — personal keys
# User view — personal keys + config snippets
from core.config_snippets import get_supported_clients
from core.site_api import get_user_sites
from core.user_keys import get_user_key_manager
user_keys = []
@@ -3019,6 +3035,8 @@ async def dashboard_keys_unified(request: Request) -> Response:
except RuntimeError:
pass
sites = await get_user_sites(user_session["user_id"])
return templates.TemplateResponse(
request,
"dashboard/keys/list.html",
@@ -3028,6 +3046,8 @@ async def dashboard_keys_unified(request: Request) -> Response:
"session": user_session,
"is_admin": False,
"user_keys": user_keys,
"sites": sites,
"clients": get_supported_clients(),
"current_page": "keys",
},
)
@@ -3153,43 +3173,13 @@ async def api_test_site(request: Request) -> Response:
async def dashboard_sites_edit(request: Request) -> Response:
"""GET /dashboard/sites/{id}/edit — Render the Edit Site form."""
user_session, redirect = _require_user_session(request)
if redirect:
return redirect
"""GET /dashboard/sites/{id}/edit — Redirect to unified site page (F.7c)."""
site_id = request.path_params.get("id", "")
from core.site_api import get_user_credential_fields, get_user_plugin_names, get_user_site
site = await get_user_site(site_id, user_session["user_id"])
if site is None:
return RedirectResponse("/dashboard/sites?error=site_not_found", status_code=302)
accept_language = request.headers.get("accept-language")
query_lang = request.query_params.get("lang")
lang = detect_language(accept_language, query_lang)
t = get_translations(lang)
import json
plugin_fields = get_user_credential_fields()
plugin_names = get_user_plugin_names()
return templates.TemplateResponse(
request,
"dashboard/sites/edit.html",
{
"lang": lang,
"t": t,
"session": user_session,
"site": site,
"plugin_fields": plugin_fields,
"plugin_fields_json": json.dumps(plugin_fields),
"plugin_names": plugin_names,
"current_page": "my_sites",
},
)
url = f"/dashboard/sites/{site_id}"
if query_lang:
url += f"?lang={query_lang}"
return RedirectResponse(url, status_code=301)
async def api_update_site(request: Request) -> Response:
@@ -3243,11 +3233,25 @@ async def api_create_key(request: Request) -> Response:
try:
key_mgr = get_user_key_manager()
# Validate site_id if provided (must belong to user)
site_id = body.get("site_id")
if site_id:
from core.database import get_database
db = get_database()
site = await db.get_site(site_id, user_session["user_id"])
if site is None:
return JSONResponse({"error": "Site not found"}, status_code=404)
# F.7c: All user keys get full access — tool visibility is controlled
# per-site via tool_scope and per-tool toggles.
result = await key_mgr.create_key(
user_id=user_session["user_id"],
name=body.get("name", "Default"),
scopes=body.get("scopes", "read write admin"),
scopes="read write admin",
expires_in_days=body.get("expires_in_days"),
site_id=site_id,
)
return JSONResponse({"key": result})
except RuntimeError as e:
@@ -3828,9 +3832,9 @@ def register_dashboard_routes(mcp):
mcp.custom_route("/dashboard/sites/add", methods=["GET"])(dashboard_sites_add)
mcp.custom_route("/dashboard/sites/{id}/edit", methods=["GET"])(dashboard_sites_edit)
mcp.custom_route("/dashboard/sites/{id}", methods=["GET"])(dashboard_sites_view)
# /dashboard/connect → /dashboard/keys (301)
# /dashboard/connect → /dashboard/sites (F.7c: sites page has connect info now)
mcp.custom_route("/dashboard/connect", methods=["GET"])(
lambda r: RedirectResponse("/dashboard/keys", status_code=301)
lambda r: RedirectResponse("/dashboard/sites", status_code=301)
)
# Service pages (F.3)