- Add interactive API Browser to Settings > API Keys

- New APIBrowser component with full OpenAPI schema integration
    - Fetches and parses /openapi.json automatically
    - Groups endpoints by API tags (printers, archives, settings, etc.)
    - Expandable endpoint sections with color-coded method badges
    - Path parameter, query parameter, and JSON body editors
    - Auto-populates request body with schema examples
    - Live API request execution with response display
    - Response shows status code, timing, and formatted JSON
    - Copy response button with clipboard fallback
    - Search to filter endpoints across all categories
    - Expand All / Collapse All buttons
    - Link to Swagger UI (/docs)

  - Two-column layout for API Keys tab
    - Left: API key management + webhook documentation
    - Right: API Browser with dedicated test key input

  - Parameter validation
    - Shows warning for missing required parameters
    - Validates before sending requests to avoid 422 errors

  - UX improvements
    - "Use in API Browser" button on newly created keys
    - Responsive layout (stacked on mobile, side-by-side on xl+)
This commit is contained in:
maziggy
2026-01-02 15:00:21 +01:00
parent 61c8682fa9
commit 83cbac04b7
102 changed files with 2719 additions and 2444 deletions
+22 -14
View File
@@ -7,9 +7,9 @@ that could cause runtime errors but aren't caught by normal tests.
import ast
import os
import pytest
from pathlib import Path
import pytest
# Get the backend source directory
BACKEND_DIR = Path(__file__).parent.parent.parent / "app"
@@ -18,8 +18,22 @@ BACKEND_DIR = Path(__file__).parent.parent.parent / "app"
# Safe imports that are commonly re-imported in functions without issues
# These are typically imported at the START of a function, not midway through
SAFE_REIMPORT_NAMES = {
'logging', 're', 'os', 'sys', 'json', 'Path', 'datetime', 'timedelta',
'asyncio', 'time', 'typing', 'Optional', 'List', 'Dict', 'Any', 'Union',
"logging",
"re",
"os",
"sys",
"json",
"Path",
"datetime",
"timedelta",
"asyncio",
"time",
"typing",
"Optional",
"List",
"Dict",
"Any",
"Union",
}
@@ -68,16 +82,10 @@ class DangerousImportVisitor(ast.NodeVisitor):
for child in ast.walk(node):
# Find local imports
if isinstance(child, (ast.Import, ast.ImportFrom)):
if isinstance(child, ast.Import):
for alias in child.names:
name = alias.asname or alias.name
if name in self.module_imports and name not in SAFE_REIMPORT_NAMES:
local_imports[name] = child.lineno
elif isinstance(child, ast.ImportFrom):
for alias in child.names:
name = alias.asname or alias.name
if name in self.module_imports and name not in SAFE_REIMPORT_NAMES:
local_imports[name] = child.lineno
for alias in child.names:
name = alias.asname or alias.name
if name in self.module_imports and name not in SAFE_REIMPORT_NAMES:
local_imports[name] = child.lineno
# Find name uses
if isinstance(child, ast.Name):
@@ -130,7 +138,7 @@ def find_import_shadowing(file_path: Path) -> list[tuple[str, int, str]]:
Returns list of (name, line_number, function_name) tuples.
"""
try:
with open(file_path, 'r') as f:
with open(file_path) as f:
source = f.read()
tree = ast.parse(source)
visitor = DangerousImportVisitor()