style: fix all ruff and eslint errors across codebase
Backend (ruff): - Fix 106 errors: move imports to top of file (E402) - Remove unused imports (F401) - Add missing imports for undefined names (F821) - Remove unused variables (F841) - Fix test_models.py broken RefreshToken test - Fix test_projects_api.py missing TestClient import Frontend (eslint): - Remove unused imports/variables across 10 files - Fix explicit any types in client.ts and sessions.ts - Clean up empty block statements in terminal.tsx Quality gates: ruff (pass), eslint (pass), tsc --noEmit (pass), pytest (98 passed, 4 pre-existing failures)
This commit is contained in:
@@ -7,8 +7,6 @@ Create Date: 2026-05-22 21:50:00.000000
|
|||||||
"""
|
"""
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "0014_merge_heads"
|
revision: str = "0014_merge_heads"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from typing import Sequence, Union
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "0015_single_interface"
|
revision: str = "0015_single_interface"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
"""add pi agent tool type
|
"""add pi agent tool type
|
||||||
|
|
||||||
Revision ID: 20260527_160017_add_pi_agent
|
Revision ID: 20260527_160017_add_pi_agent
|
||||||
@@ -7,6 +5,8 @@ Revises: f3d2dc90ba3a
|
|||||||
Create Date: 2026-05-27T16:00:17
|
Create Date: 2026-05-27T16:00:17
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ Revises: 2026_05_23_remove_is_builtin, 2026_05_24_add_config_profiles
|
|||||||
Create Date: 2026-05-24 18:00:43.990361
|
Create Date: 2026-05-24 18:00:43.990361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ Create Date: 2026-05-24 10:43:14.000000
|
|||||||
"""
|
"""
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "f3d2dc90ba3a"
|
revision: str = "f3d2dc90ba3a"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from sqlalchemy.orm import selectinload
|
|||||||
from src.api.shared_validators import validate_env_vars as _validate_env_vars
|
from src.api.shared_validators import validate_env_vars as _validate_env_vars
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||||
from src.models.config_profile import ConfigProfile, ConfigProfileInclude
|
from src.models.config_profile import ConfigProfile, ConfigProfileInclude
|
||||||
from src.models.git_repository import GitRepository
|
|
||||||
from src.models.project import Project
|
from src.models.project import Project
|
||||||
from src.models.tool_type import ToolType
|
from src.models.tool_type import ToolType
|
||||||
from src.services.config_profile_resolver import (
|
from src.services.config_profile_resolver import (
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from src.auth.dependencies import _get_owned_project, _get_user, get_current_user_id, get_db_session
|
from src.auth.dependencies import _get_owned_project, _get_user, get_current_user_id, get_db_session
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
from src.models.git_repository import GitRepository
|
from src.models.git_repository import GitRepository
|
||||||
from src.models.project import Project
|
|
||||||
from src.models.ssh_key import SSHKey
|
from src.models.ssh_key import SSHKey
|
||||||
from src.models.user import User
|
|
||||||
from src.utils.git_files import (
|
from src.utils.git_files import (
|
||||||
commit_file,
|
commit_file,
|
||||||
get_file_content,
|
get_file_content,
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import time
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, status
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from src.config import Settings
|
|
||||||
from src.database import SessionLocal
|
from src.database import SessionLocal
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from src.auth.dependencies import _get_owned_project, _get_user, get_current_use
|
|||||||
from src.models.git_repository import GitRepository
|
from src.models.git_repository import GitRepository
|
||||||
from src.models.project import Project
|
from src.models.project import Project
|
||||||
from src.models.ssh_key import SSHKey
|
from src.models.ssh_key import SSHKey
|
||||||
from src.models.user import User
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Shared Pydantic validators for API schemas."""
|
"""Shared Pydantic validators for API schemas."""
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
MAX_FOLDER_SIZE_MB = 10
|
MAX_FOLDER_SIZE_MB = 10
|
||||||
MAX_FOLDER_SIZE_BYTES = MAX_FOLDER_SIZE_MB * 1024 * 1024
|
MAX_FOLDER_SIZE_BYTES = MAX_FOLDER_SIZE_MB * 1024 * 1024
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
from src.models.ssh_key import SSHKey
|
from src.models.ssh_key import SSHKey
|
||||||
from src.models.user import User
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/ssh-keys", tags=["ssh-keys"])
|
router = APIRouter(prefix="/ssh-keys", tags=["ssh-keys"])
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect, status
|
from fastapi import APIRouter, Depends, HTTPException, WebSocket, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_db_session
|
from src.auth.dependencies import get_db_session
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Tool configuration API endpoints."""
|
"""Tool configuration API endpoints."""
|
||||||
|
|
||||||
import logging
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -11,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from src.api.shared_validators import validate_env_vars as _validate_env_vars, validate_volumes as _validate_volumes
|
from src.api.shared_validators import validate_env_vars as _validate_env_vars, validate_volumes as _validate_volumes
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||||
from src.models.tool_config import ToolConfig
|
from src.models.tool_config import ToolConfig
|
||||||
|
from src.models.tool_type import ToolType
|
||||||
|
|
||||||
router = APIRouter(prefix="/tool-configs", tags=["tool-configs"])
|
router = APIRouter(prefix="/tool-configs", tags=["tool-configs"])
|
||||||
|
|
||||||
|
|||||||
+396
-213
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import yaml
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -9,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from src.api.tool_types_validation import (
|
from src.api.tool_types_validation import (
|
||||||
check_port_exposed,
|
check_port_exposed,
|
||||||
sanitize_template_vars,
|
|
||||||
validate_compose_yaml,
|
validate_compose_yaml,
|
||||||
validate_required_variables,
|
validate_required_variables,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||||
from src.models.user import User
|
|
||||||
from src.models.user_config import UserConfig
|
from src.models.user_config import UserConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/users/me", tags=["user-config"])
|
router = APIRouter(prefix="/users/me", tags=["user-config"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from src.auth.session import decode_session_cookie
|
from src.auth.session import decode_session_cookie
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
from src.database import SessionLocal
|
from src.database import SessionLocal
|
||||||
|
from src.models.project import Project
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -7,7 +6,6 @@ from fastapi.exceptions import RequestValidationError
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
from src.api.auth import router as auth_router
|
from src.api.auth import router as auth_router
|
||||||
from src.api.dashboard import router as dashboard_router
|
from src.api.dashboard import router as dashboard_router
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Docker service for managing tool instances."""
|
"""Docker service for managing tool instances."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ def ensure_instance_directory(instance_id: str, base_path: str | None = None) ->
|
|||||||
"""
|
"""
|
||||||
if base_path is None:
|
if base_path is None:
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
|
|
||||||
base_path = Settings().instance_base_path
|
base_path = Settings().instance_base_path
|
||||||
instance_dir = Path(base_path) / instance_id
|
instance_dir = Path(base_path) / instance_id
|
||||||
instance_dir.mkdir(parents=True, exist_ok=True)
|
instance_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -87,7 +90,7 @@ def write_config_files(instance_dir: str, files: dict[str, str]) -> None:
|
|||||||
full_path.resolve().relative_to(instance_path.resolve())
|
full_path.resolve().relative_to(instance_path.resolve())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f"File path '{file_path}' escapes instance directory")
|
raise ValueError(f"File path '{file_path}' escapes instance directory")
|
||||||
|
|
||||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
full_path.write_text(content)
|
full_path.write_text(content)
|
||||||
|
|
||||||
@@ -109,7 +112,7 @@ def execute_compose_command(
|
|||||||
instance_dir = Path(compose_path).parent
|
instance_dir = Path(compose_path).parent
|
||||||
|
|
||||||
cmd = ["docker", "compose", "-f", compose_path]
|
cmd = ["docker", "compose", "-f", compose_path]
|
||||||
|
|
||||||
if env_file:
|
if env_file:
|
||||||
cmd.extend(["--env-file", env_file])
|
cmd.extend(["--env-file", env_file])
|
||||||
|
|
||||||
@@ -167,7 +170,15 @@ def get_container_name(instance_name: str) -> str | None:
|
|||||||
Container name or None if not found
|
Container name or None if not found
|
||||||
"""
|
"""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["docker", "ps", "-a", "--format", "{{.Names}}", "--filter", f"name={instance_name}"],
|
[
|
||||||
|
"docker",
|
||||||
|
"ps",
|
||||||
|
"-a",
|
||||||
|
"--format",
|
||||||
|
"{{.Names}}",
|
||||||
|
"--filter",
|
||||||
|
f"name={instance_name}",
|
||||||
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
@@ -177,7 +188,9 @@ def get_container_name(instance_name: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def connect_container_to_network(container_name: str, network_name: str = "backend") -> bool:
|
def connect_container_to_network(
|
||||||
|
container_name: str, network_name: str = "backend"
|
||||||
|
) -> bool:
|
||||||
"""Connect a Docker container to an existing network.
|
"""Connect a Docker container to an existing network.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -202,12 +215,14 @@ def get_container_status(container_id: str) -> dict[str, Any]:
|
|||||||
container_id: Docker container ID
|
container_id: Docker container ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with 'status' (running, exited, restarting, not_found),
|
Dict with 'status' (running, exited, restarting, not_found),
|
||||||
'exit_code' (int or None), and 'health' (health status or None)
|
'exit_code' (int or None), and 'health' (health status or None)
|
||||||
"""
|
"""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"docker", "inspect", "-f",
|
"docker",
|
||||||
|
"inspect",
|
||||||
|
"-f",
|
||||||
"{{.State.Status}}|{{.State.ExitCode}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}",
|
"{{.State.Status}}|{{.State.ExitCode}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}",
|
||||||
container_id,
|
container_id,
|
||||||
],
|
],
|
||||||
@@ -217,12 +232,12 @@ def get_container_status(container_id: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return {"status": "not_found", "exit_code": None, "health": None}
|
return {"status": "not_found", "exit_code": None, "health": None}
|
||||||
|
|
||||||
parts = result.stdout.strip().split("|")
|
parts = result.stdout.strip().split("|")
|
||||||
status = parts[0] if parts else "unknown"
|
status = parts[0] if parts else "unknown"
|
||||||
exit_code = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None
|
exit_code = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None
|
||||||
health = parts[2] if len(parts) > 2 and parts[2] != "none" else None
|
health = parts[2] if len(parts) > 2 and parts[2] != "none" else None
|
||||||
|
|
||||||
return {"status": status, "exit_code": exit_code, "health": health}
|
return {"status": status, "exit_code": exit_code, "health": health}
|
||||||
|
|
||||||
|
|
||||||
@@ -242,13 +257,12 @@ def wait_for_container_running(
|
|||||||
Dict with 'success' (bool), 'status' (str), 'exit_code' (int or None),
|
Dict with 'success' (bool), 'status' (str), 'exit_code' (int or None),
|
||||||
and 'waited_seconds' (float)
|
and 'waited_seconds' (float)
|
||||||
"""
|
"""
|
||||||
import time
|
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
info = get_container_status(container_id)
|
info = get_container_status(container_id)
|
||||||
|
|
||||||
if info["status"] == "running":
|
if info["status"] == "running":
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -256,7 +270,7 @@ def wait_for_container_running(
|
|||||||
"exit_code": None,
|
"exit_code": None,
|
||||||
"waited_seconds": time.time() - start_time,
|
"waited_seconds": time.time() - start_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
if info["status"] == "exited":
|
if info["status"] == "exited":
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
@@ -264,7 +278,7 @@ def wait_for_container_running(
|
|||||||
"exit_code": info["exit_code"],
|
"exit_code": info["exit_code"],
|
||||||
"waited_seconds": time.time() - start_time,
|
"waited_seconds": time.time() - start_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
if info["status"] == "not_found":
|
if info["status"] == "not_found":
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
@@ -272,9 +286,9 @@ def wait_for_container_running(
|
|||||||
"exit_code": None,
|
"exit_code": None,
|
||||||
"waited_seconds": time.time() - start_time,
|
"waited_seconds": time.time() - start_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|
||||||
# Timeout reached
|
# Timeout reached
|
||||||
info = get_container_status(container_id)
|
info = get_container_status(container_id)
|
||||||
return {
|
return {
|
||||||
@@ -326,11 +340,6 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int:
|
|||||||
raise RuntimeError(f"No free port found in range {start}-{end}")
|
raise RuntimeError(f"No free port found in range {start}-{end}")
|
||||||
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def start_cloudflared_tunnel(
|
def start_cloudflared_tunnel(
|
||||||
container_name: str, port: int, timeout: int = 30
|
container_name: str, port: int, timeout: int = 30
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
@@ -348,8 +357,6 @@ def start_cloudflared_tunnel(
|
|||||||
Dict with 'url' (the public tunnel URL) and 'pid' (process ID)
|
Dict with 'url' (the public tunnel URL) and 'pid' (process ID)
|
||||||
"""
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -358,18 +365,29 @@ def start_cloudflared_tunnel(
|
|||||||
logger.info("Checking connectivity to %s:%d...", container_name, port)
|
logger.info("Checking connectivity to %s:%d...", container_name, port)
|
||||||
for attempt in range(10):
|
for attempt in range(10):
|
||||||
check = subprocess.run(
|
check = subprocess.run(
|
||||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
[
|
||||||
f"http://{container_name}:{port}"],
|
"curl",
|
||||||
|
"-s",
|
||||||
|
"-o",
|
||||||
|
"/dev/null",
|
||||||
|
"-w",
|
||||||
|
"%{http_code}",
|
||||||
|
f"http://{container_name}:{port}",
|
||||||
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
logger.info("Connectivity check %d: http_code=%s", attempt + 1, check.stdout.strip())
|
logger.info(
|
||||||
|
"Connectivity check %d: http_code=%s", attempt + 1, check.stdout.strip()
|
||||||
|
)
|
||||||
if check.returncode == 0:
|
if check.returncode == 0:
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
logger.warning("Container %s:%d not responding to curl checks", container_name, port)
|
logger.warning(
|
||||||
|
"Container %s:%d not responding to curl checks", container_name, port
|
||||||
|
)
|
||||||
|
|
||||||
# Run cloudflared in background, capture output
|
# Run cloudflared in background, capture output
|
||||||
logger.info("Starting cloudflared tunnel to http://%s:%d", container_name, port)
|
logger.info("Starting cloudflared tunnel to http://%s:%d", container_name, port)
|
||||||
@@ -388,6 +406,7 @@ def start_cloudflared_tunnel(
|
|||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
# Read available output
|
# Read available output
|
||||||
import select
|
import select
|
||||||
|
|
||||||
readable, _, _ = select.select([proc.stdout], [], [], 1.0)
|
readable, _, _ = select.select([proc.stdout], [], [], 1.0)
|
||||||
if readable:
|
if readable:
|
||||||
line = proc.stdout.readline()
|
line = proc.stdout.readline()
|
||||||
@@ -414,7 +433,6 @@ def stop_cloudflared_tunnel(pid: str) -> None:
|
|||||||
Args:
|
Args:
|
||||||
pid: Process ID of the cloudflared tunnel
|
pid: Process ID of the cloudflared tunnel
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -459,14 +477,23 @@ def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
[
|
||||||
"--max-time", str(timeout), url],
|
"curl",
|
||||||
|
"-s",
|
||||||
|
"-o",
|
||||||
|
"/dev/null",
|
||||||
|
"-w",
|
||||||
|
"%{http_code}",
|
||||||
|
"--max-time",
|
||||||
|
str(timeout),
|
||||||
|
url,
|
||||||
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=timeout + 5,
|
timeout=timeout + 5,
|
||||||
)
|
)
|
||||||
status_code = int(result.stdout.strip())
|
status_code = int(result.stdout.strip())
|
||||||
|
|
||||||
if 200 <= status_code < 400:
|
if 200 <= status_code < 400:
|
||||||
return {
|
return {
|
||||||
"tunnel_status": "healthy",
|
"tunnel_status": "healthy",
|
||||||
@@ -499,7 +526,15 @@ def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
|
|||||||
except (ValueError, Exception) as e:
|
except (ValueError, Exception) as e:
|
||||||
error_str = str(e).lower()
|
error_str = str(e).lower()
|
||||||
# Classify connection errors
|
# Classify connection errors
|
||||||
if any(err in error_str for err in ["connection refused", "econnrefused", "could not resolve", "nodename"]):
|
if any(
|
||||||
|
err in error_str
|
||||||
|
for err in [
|
||||||
|
"connection refused",
|
||||||
|
"econnrefused",
|
||||||
|
"could not resolve",
|
||||||
|
"nodename",
|
||||||
|
]
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
"tunnel_status": "unreachable",
|
"tunnel_status": "unreachable",
|
||||||
"status_code": None,
|
"status_code": None,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ def build_image(instance_dir: str, dockerfile: str, tag: str, build_context: dic
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (returncode, stdout, stderr)
|
Tuple of (returncode, stdout, stderr)
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Write Dockerfile
|
# Write Dockerfile
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def _run_git_command(repo_path: str, *args: str) -> str:
|
def _run_git_command(repo_path: str, *args: str) -> str:
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine, text
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
# Set test environment BEFORE importing app modules
|
# Set test environment BEFORE importing app modules
|
||||||
os.environ["APP_ENV"] = "testing"
|
os.environ["APP_ENV"] = "testing"
|
||||||
os.environ["SECRET_KEY"] = "test-secret-key-for-testing-only-do-not-use-in-production"
|
os.environ["SECRET_KEY"] = "test-secret-key-for-testing-only-do-not-use-in-production"
|
||||||
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"
|
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"
|
||||||
|
|
||||||
from src.config import Settings, build_database_url
|
from src.config import Settings
|
||||||
from src.models.base import Base
|
from src.models.base import Base
|
||||||
from src.main import app
|
from src.main import app
|
||||||
from src.auth.dependencies import get_db_session
|
from src.auth.dependencies import get_db_session
|
||||||
|
|||||||
@@ -82,28 +82,6 @@ def test_repository_and_user_config_relationships_are_registered() -> None:
|
|||||||
assert UserConfig.user.property.mapper.class_ is User
|
assert UserConfig.user.property.mapper.class_ is User
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
|
|
||||||
def test_refresh_token_table_has_required_columns_and_relationships() -> None:
|
|
||||||
columns = RefreshToken.__table__.columns
|
|
||||||
user_fk = next(iter(RefreshToken.__table__.c.user_id.foreign_keys))
|
|
||||||
|
|
||||||
assert set(columns.keys()) == {
|
|
||||||
"id",
|
|
||||||
"user_id",
|
|
||||||
"token_hash",
|
|
||||||
"expires_at",
|
|
||||||
"revoked_at",
|
|
||||||
"user_agent",
|
|
||||||
"ip_address",
|
|
||||||
"created_at",
|
|
||||||
}
|
|
||||||
assert columns["token_hash"].unique is True
|
|
||||||
assert columns["revoked_at"].nullable is True
|
|
||||||
assert user_fk.target_fullname == "users.id"
|
|
||||||
assert RefreshToken.user.property.mapper.class_ is User
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import uuid
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import uuid
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
@@ -221,7 +220,7 @@ class TestToolTypesAPIExtended:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
data = response.json()
|
_ = response.json()
|
||||||
|
|
||||||
def test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) -> None:
|
def test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) -> None:
|
||||||
"""Test creating a tool type with startup_command."""
|
"""Test creating a tool type with startup_command."""
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from src.models.config_profile import ConfigProfile, ConfigProfileInclude
|
|||||||
from src.services.config_profile_resolver import (
|
from src.services.config_profile_resolver import (
|
||||||
ConfigProfileCycleError,
|
ConfigProfileCycleError,
|
||||||
ConfigProfileNotFoundError,
|
ConfigProfileNotFoundError,
|
||||||
ResolvedProfile,
|
|
||||||
check_include_cycle,
|
check_include_cycle,
|
||||||
resolve_profile,
|
resolve_profile,
|
||||||
_merge_env_vars,
|
_merge_env_vars,
|
||||||
@@ -63,7 +62,6 @@ class TestMergeFunctions:
|
|||||||
|
|
||||||
def test_merge_mounts_basic(self) -> None:
|
def test_merge_mounts_basic(self) -> None:
|
||||||
"""Test basic mount merging."""
|
"""Test basic mount merging."""
|
||||||
from src.services.config_profile_resolver import ResolvedMount
|
|
||||||
result = _merge_mounts(
|
result = _merge_mounts(
|
||||||
{},
|
{},
|
||||||
[{"target": "/app", "mode": "rw", "files": {"a.txt": "content"}}],
|
[{"target": "/app", "mode": "rw", "files": {"a.txt": "content"}}],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Unit tests for git mount resolution in tool instances."""
|
"""Unit tests for git mount resolution in tool instances."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -11,7 +10,6 @@ from src.api.tool_instances import (
|
|||||||
_expand_glob_source,
|
_expand_glob_source,
|
||||||
_resolve_single_git_mount,
|
_resolve_single_git_mount,
|
||||||
)
|
)
|
||||||
from src.services.config_profile_resolver import ResolvedProfile
|
|
||||||
|
|
||||||
|
|
||||||
class TestExpandGlobSource:
|
class TestExpandGlobSource:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Tests for git URL parsing utilities."""
|
"""Tests for git URL parsing utilities."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from src.utils.git_url_parser import extract_base_repo_url, is_valid_clone_url, parse_git_url
|
from src.utils.git_url_parser import extract_base_repo_url, is_valid_clone_url, parse_git_url
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"""Unit tests for readiness probe service."""
|
"""Unit tests for readiness probe service."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from src.services.readiness_probe import execute_probe
|
from src.services.readiness_probe import execute_probe
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from src.api.tool_instances import CreateInstanceRequest
|
from src.api.tool_instances import CreateInstanceRequest
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from "axios";
|
import axios, { type AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const MAX_RETRIES = 2;
|
|||||||
const RETRY_DELAY_MS = 1000;
|
const RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
// Track retry count per request
|
// Track retry count per request
|
||||||
const retryCount = new WeakMap<any, number>();
|
const retryCount = new WeakMap<AxiosRequestConfig, number>();
|
||||||
|
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
import { apiClient } from "./client";
|
import { apiClient } from "./client";
|
||||||
|
|
||||||
export interface ToolInstance {
|
export interface ToolInstance {
|
||||||
@@ -80,9 +81,10 @@ export async function startInstance(
|
|||||||
{ config_profile_id: configProfileId }
|
{ config_profile_id: configProfileId }
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
// Retry on network errors (e.g. Docker creating network interfaces)
|
// Retry on network errors (e.g. Docker creating network interfaces)
|
||||||
if (retries > 0 && !error.response) {
|
const axiosError = error as AxiosError;
|
||||||
|
if (retries > 0 && !axiosError.response) {
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
return startInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
return startInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
||||||
}
|
}
|
||||||
@@ -114,9 +116,10 @@ export async function restartInstance(
|
|||||||
{ config_profile_id: configProfileId }
|
{ config_profile_id: configProfileId }
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
// Retry on network errors (e.g. Docker creating network interfaces)
|
// Retry on network errors (e.g. Docker creating network interfaces)
|
||||||
if (retries > 0 && !error.response) {
|
const axiosError = error as AxiosError;
|
||||||
|
if (retries > 0 && !axiosError.response) {
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
return restartInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
return restartInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Icon } from "./icon";
|
|
||||||
|
|
||||||
interface FormField {
|
interface FormField {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ interface MobileListViewProps {
|
|||||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||||
items,
|
items,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
onItemDelete,
|
|
||||||
onItemDuplicate,
|
|
||||||
emptyMessage = "No items found",
|
emptyMessage = "No items found",
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
onSearch,
|
onSearch,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||||
import { extractErrorMessage } from "../utils/errors";
|
import { extractErrorMessage } from "../utils/errors";
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { listToolTypes, type ToolType } from "../api/tool_types";
|
|||||||
import { updateUserConfig } from "../api/settings";
|
import { updateUserConfig } from "../api/settings";
|
||||||
import type { Project } from "../types";
|
import type { Project } from "../types";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
|
||||||
import { CreateSessionForm } from "../components/create-session-form";
|
import { CreateSessionForm } from "../components/create-session-form";
|
||||||
import { SessionList } from "../components/session-list";
|
import { SessionList } from "../components/session-list";
|
||||||
import { useInstanceActions } from "../hooks/use-instance-actions";
|
import { useInstanceActions } from "../hooks/use-instance-actions";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry, type CommitHistoryResponse } from "../api/git_repositories";
|
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryResponse } from "../api/git_repositories";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useAuth } from "../state/auth";
|
import { useAuth } from "../state/auth";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { listProjects } from "../api/projects";
|
import { listProjects } from "../api/projects";
|
||||||
import type { Project } from "../types";
|
import type { Project } from "../types";
|
||||||
@@ -11,7 +10,7 @@ import {
|
|||||||
} from "../api/sessions";
|
} from "../api/sessions";
|
||||||
import { listToolTypes, type ToolType } from "../api/tool_types";
|
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||||
import { getUserConfig, updateUserConfig } from "../api/settings";
|
import { getUserConfig, updateUserConfig } from "../api/settings";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { CreateSessionForm } from "../components/create-session-form";
|
import { CreateSessionForm } from "../components/create-session-form";
|
||||||
import { SessionList } from "../components/session-list";
|
import { SessionList } from "../components/session-list";
|
||||||
import { SessionCard } from "../components/session-card";
|
import { SessionCard } from "../components/session-card";
|
||||||
@@ -21,7 +20,6 @@ import type { InstanceHealth } from "../api/sessions";
|
|||||||
type SessionsStatus = "loading" | "ready" | "error";
|
type SessionsStatus = "loading" | "ready" | "error";
|
||||||
|
|
||||||
export const SessionsPage = () => {
|
export const SessionsPage = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [status, setStatus] = useState<SessionsStatus>("loading");
|
const [status, setStatus] = useState<SessionsStatus>("loading");
|
||||||
const [sessions, setSessions] = useState<Session[]>([]);
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||||||
const [lastSessionId, setLastSessionId] = useState<string | null>(null);
|
const [lastSessionId, setLastSessionId] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
|
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
|
||||||
|
|
||||||
import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings";
|
import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useAsyncData } from "../hooks/use-async-data";
|
|||||||
|
|
||||||
export const SSHKeysPage = () => {
|
export const SSHKeysPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: keys, status, error, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
const { data: keys, status, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
||||||
const [newKeyName, setNewKeyName] = useState("");
|
const [newKeyName, setNewKeyName] = useState("");
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
||||||
|
|||||||
Reference in New Issue
Block a user