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

@@ -4,11 +4,13 @@ Tests covering dashboard authentication, session management, rate limiting,
language detection, translations, and utility functions.
"""
import inspect
import time
from datetime import UTC, datetime, timedelta
import jwt
import pytest
from starlette.requests import Request
from core.dashboard.auth import DashboardAuth, DashboardSession
from core.dashboard.routes import (
@@ -19,6 +21,29 @@ from core.dashboard.routes import (
get_translations,
)
def _make_request(path: str, *, query_string: str = "", path_params: dict[str, str] | None = None):
"""Construct a minimal GET Request for direct route-handler tests."""
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
scope = {
"type": "http",
"http_version": "1.1",
"scheme": "http",
"method": "GET",
"path": path,
"raw_path": path.encode(),
"query_string": query_string.encode(),
"headers": [],
"path_params": path_params or {},
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
return Request(scope, receive)
# --- Plugin Display Names ---
@@ -36,26 +61,27 @@ class TestPluginDisplayNames:
assert get_plugin_display_name("appwrite") == "Appwrite"
assert get_plugin_display_name("directus") == "Directus"
def test_wordpress_advanced(self):
def test_wordpress_specialist(self):
"""Should handle underscore-separated names."""
assert get_plugin_display_name("wordpress_advanced") == "WordPress Advanced"
assert get_plugin_display_name("wordpress_specialist") == "WordPress Specialist"
def test_unknown_plugin_titlecased(self):
"""Should title-case unknown plugin types."""
assert get_plugin_display_name("my_custom_plugin") == "My Custom Plugin"
def test_all_nine_plugins_mapped(self):
"""All 9 plugin types should be in the display names map."""
def test_all_plugins_mapped(self):
"""Every registered plugin type must be in the display names map."""
expected = {
"wordpress",
"woocommerce",
"wordpress_advanced",
"wordpress_specialist",
"gitea",
"n8n",
"supabase",
"openpanel",
"appwrite",
"directus",
"coolify",
}
assert expected == set(PLUGIN_DISPLAY_NAMES.keys())
@@ -373,16 +399,46 @@ class TestDashboardCookieManagement:
assert "mcp_dashboard_session=" in cookie_header
def test_dashboard_connect_page(monkeypatch):
"""Test that /dashboard/connect redirects to /dashboard/keys (F.7b session 2)."""
from server import create_multi_endpoint_app
from starlette.testclient import TestClient
@pytest.mark.asyncio
async def test_dashboard_connect_page(monkeypatch):
"""Legacy /dashboard-legacy/connect should redirect to legacy keys."""
from core.dashboard.routes import register_dashboard_routes
app = create_multi_endpoint_app()
client = TestClient(app, follow_redirects=False)
class FakeMCP:
def __init__(self):
self.routes = {}
resp = client.get("/dashboard/connect")
def custom_route(self, path, methods=None):
def decorator(func):
self.routes[(path, tuple(methods or []))] = func
return func
# /dashboard/connect now redirects 301 to /dashboard/keys
assert resp.status_code == 301
assert "/dashboard/keys" in resp.headers["location"]
return decorator
mcp = FakeMCP()
register_dashboard_routes(mcp)
handler = mcp.routes[("/dashboard-legacy/connect", ("GET",))]
response = handler(_make_request("/dashboard-legacy/connect"))
if inspect.isawaitable(response):
response = await response
assert response.status_code == 301
assert response.headers["location"] == "/dashboard-legacy/keys"
@pytest.mark.asyncio
async def test_dashboard_v2_redirects_to_dashboard(monkeypatch):
"""Old SPA URLs should 308 redirect to the cutover /dashboard prefix."""
from core.dashboard.spa_routes import redirect_dashboard_v2
response = await redirect_dashboard_v2(
_make_request(
"/dashboard-v2/overview",
query_string="lang=fa",
path_params={"path": "overview"},
)
)
assert response.status_code == 308
assert response.headers["location"] == "/dashboard/overview?lang=fa"