fix: tunnel URLs, session naming, git control bar placement
- Exclude api.trycloudflare.com from tunnel URL regex (both tunnel.py and docker/tunnel.py). Real tunnel subdomains are 10+ random chars. - Fix missing get_backend_network_name import in services/tunnel.py by using hardcoded BACKEND_NETWORK constant. - Add auto-numbering to session display names: project/repo/tool #N when duplicates exist for the same project/repo/tool type. - Move GitToolbar from full-width workspace header into WorkspaceSidebar above the FileBrowser, so git controls appear in the file browser area. - Remove stale ToolConfig import from instance_lifecycle.py (model was removed in earlier dev work); _fetch_tool_configs returns empty defaults. Quality gates: tsc pass (0 errors), build pass, Python syntax pass Tests: 127/131 passed (4 pre-existing failures)
This commit is contained in:
@@ -56,7 +56,9 @@ def start_cloudflared_tunnel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Wait for the URL to appear in output
|
# Wait for the URL to appear in output
|
||||||
url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
# Exclude api.trycloudflare.com which is the Cloudflare API endpoint,
|
||||||
|
# not a tunnel URL. Real tunnel URLs have random subdomains.
|
||||||
|
url_pattern = re.compile(r"https://(?!api\.)[a-z0-9-]{10,}\.trycloudflare\.com")
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
url = None
|
url = None
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from src.models.config_folder import ConfigFolder
|
|||||||
from src.models.config_profile import ConfigProfile
|
from src.models.config_profile import ConfigProfile
|
||||||
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.tool_config import ToolConfig
|
# ToolConfig model was removed; _fetch_tool_configs returns empty defaults
|
||||||
from src.models.tool_instance import ToolInstance
|
from src.models.tool_instance import ToolInstance
|
||||||
from src.models.tool_type import ToolType
|
from src.models.tool_type import ToolType
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
@@ -52,9 +52,26 @@ async def create_new_instance(
|
|||||||
tool_type, instance_name, instance_dir, repo, user, project.id, tool_port
|
tool_type, instance_name, instance_dir, repo, user, project.id, tool_port
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Auto-generate display name with numbering when duplicates exist
|
||||||
|
auto_name = f"{project.name} / {repo.name} / {tool_type.display_name}"
|
||||||
|
if not display_name:
|
||||||
|
result = await session.execute(
|
||||||
|
select(ToolInstance).where(
|
||||||
|
ToolInstance.project_id == project.id,
|
||||||
|
ToolInstance.repository_id == repo.id,
|
||||||
|
ToolInstance.tool_type_id == tool_type.id,
|
||||||
|
ToolInstance.owner_id == user.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing = result.scalars().all()
|
||||||
|
count = len(existing)
|
||||||
|
if count > 0:
|
||||||
|
auto_name = f"{project.name} / {repo.name} / {tool_type.display_name} #{count + 1}"
|
||||||
|
display_name = auto_name
|
||||||
|
|
||||||
instance = ToolInstance(
|
instance = ToolInstance(
|
||||||
name=instance_name,
|
name=instance_name,
|
||||||
display_name=display_name or f"{project.name} / {repo.name} / {tool_type.display_name}",
|
display_name=display_name,
|
||||||
tool_type_id=tool_type.id,
|
tool_type_id=tool_type.id,
|
||||||
repository_id=repo.id,
|
repository_id=repo.id,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
@@ -308,45 +325,17 @@ async def _build_or_render_compose(
|
|||||||
|
|
||||||
|
|
||||||
async def _fetch_tool_configs(
|
async def _fetch_tool_configs(
|
||||||
session: AsyncSession,
|
_session: AsyncSession,
|
||||||
user_id: Any,
|
_user_id: Any,
|
||||||
tool_type_id: Any,
|
_tool_type_id: Any,
|
||||||
project_id: Any,
|
_project_id: Any,
|
||||||
) -> tuple[dict, dict, Any, Any, Any, dict, list]:
|
) -> tuple[dict, dict, Any, Any, Any, dict, list]:
|
||||||
"""Fetch tool configs and return parsed values."""
|
"""Fetch tool configs and return parsed values.
|
||||||
env_vars: dict[str, str] = {}
|
|
||||||
config_files: dict[str, str] = {}
|
|
||||||
port_override = None
|
|
||||||
start_command = None
|
|
||||||
working_directory = None
|
|
||||||
extra_env_vars: dict[str, str] = {}
|
|
||||||
extra_volumes: list[dict] = []
|
|
||||||
|
|
||||||
query = (
|
ToolConfig model was removed from the codebase; this now returns
|
||||||
select(ToolConfig)
|
empty defaults so the lifecycle code continues to compile.
|
||||||
.where(ToolConfig.user_id == user_id, ToolConfig.tool_type_id == tool_type_id)
|
"""
|
||||||
.where((ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None)))
|
return {}, {}, None, None, None, {}, []
|
||||||
)
|
|
||||||
configs = (await session.execute(query)).scalars().all()
|
|
||||||
|
|
||||||
for cfg in configs:
|
|
||||||
if cfg.config_type == "env":
|
|
||||||
env_vars[cfg.key] = cfg.value
|
|
||||||
elif cfg.config_type == "file" and cfg.file_path:
|
|
||||||
config_files[cfg.file_path] = cfg.value
|
|
||||||
if cfg.port_override:
|
|
||||||
port_override = cfg.port_override
|
|
||||||
if cfg.start_command:
|
|
||||||
start_command = cfg.start_command
|
|
||||||
if cfg.working_directory:
|
|
||||||
working_directory = cfg.working_directory
|
|
||||||
if cfg.environment_variables:
|
|
||||||
extra_env_vars.update(cfg.environment_variables)
|
|
||||||
if cfg.volumes:
|
|
||||||
extra_volumes.extend(cfg.volumes)
|
|
||||||
|
|
||||||
env_vars.update(extra_env_vars)
|
|
||||||
return env_vars, config_files, port_override, start_command, working_directory, extra_env_vars, extra_volumes
|
|
||||||
|
|
||||||
|
|
||||||
async def _stage_configs_and_folders(
|
async def _stage_configs_and_folders(
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from src.services.docker import get_backend_network_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BACKEND_NETWORK = "backend"
|
||||||
|
|
||||||
TUNNEL_IMAGE = "cloudflare/cloudflared:latest"
|
TUNNEL_IMAGE = "cloudflare/cloudflared:latest"
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ def start_tunnel(
|
|||||||
"run",
|
"run",
|
||||||
"-d",
|
"-d",
|
||||||
"--network",
|
"--network",
|
||||||
get_backend_network_name(),
|
BACKEND_NETWORK,
|
||||||
"--name",
|
"--name",
|
||||||
tunnel_name,
|
tunnel_name,
|
||||||
TUNNEL_IMAGE,
|
TUNNEL_IMAGE,
|
||||||
@@ -133,7 +133,9 @@ def start_tunnel(
|
|||||||
logger.debug("Tunnel container started: %s", container_id)
|
logger.debug("Tunnel container started: %s", container_id)
|
||||||
|
|
||||||
# Wait for URL to appear in logs
|
# Wait for URL to appear in logs
|
||||||
url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
# Exclude api.trycloudflare.com which is the Cloudflare API endpoint,
|
||||||
|
# not a tunnel URL. Real tunnel URLs have random subdomains (10+ chars).
|
||||||
|
url_pattern = re.compile(r"https://(?!api\.)[a-z0-9-]{10,}\.trycloudflare\.com")
|
||||||
start_time = __import__("time").time()
|
start_time = __import__("time").time()
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
combined_logs = ""
|
combined_logs = ""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ToolType } from "../../../types/tool-type";
|
|||||||
import { FileBrowser } from "./FileBrowser";
|
import { FileBrowser } from "./FileBrowser";
|
||||||
import { CommitPanel } from "../git/CommitPanel";
|
import { CommitPanel } from "../git/CommitPanel";
|
||||||
import { InstanceList } from "../session/InstanceList";
|
import { InstanceList } from "../session/InstanceList";
|
||||||
|
import { GitToolbar } from "./GitToolbar";
|
||||||
|
|
||||||
interface WorkspaceSidebarProps {
|
interface WorkspaceSidebarProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -11,7 +12,12 @@ interface WorkspaceSidebarProps {
|
|||||||
repositories: GitRepository[];
|
repositories: GitRepository[];
|
||||||
gitStatus: GitStatus | null;
|
gitStatus: GitStatus | null;
|
||||||
toolTypes: ToolType[];
|
toolTypes: ToolType[];
|
||||||
|
currentBranch: string;
|
||||||
|
branches: string[];
|
||||||
|
hasRemote: boolean;
|
||||||
onRepoChange: (repoId: string) => void;
|
onRepoChange: (repoId: string) => void;
|
||||||
|
onBranchChange: (branch: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
onCommit: () => void;
|
onCommit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +27,25 @@ export const WorkspaceSidebar = ({
|
|||||||
repositories,
|
repositories,
|
||||||
gitStatus,
|
gitStatus,
|
||||||
toolTypes,
|
toolTypes,
|
||||||
|
currentBranch,
|
||||||
|
branches,
|
||||||
|
hasRemote,
|
||||||
onRepoChange,
|
onRepoChange,
|
||||||
|
onBranchChange,
|
||||||
|
onRefresh,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: WorkspaceSidebarProps) => {
|
}: WorkspaceSidebarProps) => {
|
||||||
return (
|
return (
|
||||||
<aside className="workspace-sidebar">
|
<aside className="workspace-sidebar">
|
||||||
|
<GitToolbar
|
||||||
|
projectId={projectId}
|
||||||
|
repoId={repoId}
|
||||||
|
currentBranch={currentBranch}
|
||||||
|
branches={branches}
|
||||||
|
hasRemote={hasRemote}
|
||||||
|
onBranchChange={onBranchChange}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
<div className="sidebar-section">
|
<div className="sidebar-section">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
Repository
|
Repository
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { WorkspaceSidebar } from "../components/features/git";
|
import { WorkspaceSidebar } from "../components/features/git";
|
||||||
import { FileEditor } from "../components/features/git/FileEditor";
|
import { FileEditor } from "../components/features/git/FileEditor";
|
||||||
import { WorkspaceHeader } from "../components/features/workspace/WorkspaceHeader";
|
import { WorkspaceHeader } from "../components/features/workspace/WorkspaceHeader";
|
||||||
import { GitToolbar } from "../components/features/git/GitToolbar";
|
|
||||||
import { LoadingState, ErrorState } from "../components/ui";
|
import { LoadingState, ErrorState } from "../components/ui";
|
||||||
import { listToolTypes } from "../api/tool-types";
|
import { listToolTypes } from "../api/tool-types";
|
||||||
|
|
||||||
@@ -161,26 +160,6 @@ export const RepoWorkspace = () => {
|
|||||||
|
|
||||||
{status === "ready" && repositories.length > 0 && (
|
{status === "ready" && repositories.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{selectedRepoId && (
|
|
||||||
<GitToolbar
|
|
||||||
projectId={projectId!}
|
|
||||||
repoId={selectedRepoId}
|
|
||||||
currentBranch={currentBranch}
|
|
||||||
branches={branches}
|
|
||||||
hasRemote={Boolean(selectedRepo?.remote_url)}
|
|
||||||
onBranchChange={(branch) => {
|
|
||||||
setCurrentBranch(branch);
|
|
||||||
const newParams = new URLSearchParams(searchParams);
|
|
||||||
newParams.set("branch", branch);
|
|
||||||
setSearchParams(newParams);
|
|
||||||
}}
|
|
||||||
onRefresh={() => {
|
|
||||||
void loadBranches();
|
|
||||||
void loadGitStatus();
|
|
||||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="workspace-layout">
|
<div className="workspace-layout">
|
||||||
{selectedRepoId && (
|
{selectedRepoId && (
|
||||||
<WorkspaceSidebar
|
<WorkspaceSidebar
|
||||||
@@ -189,7 +168,21 @@ export const RepoWorkspace = () => {
|
|||||||
repositories={repositories}
|
repositories={repositories}
|
||||||
gitStatus={gitStatus}
|
gitStatus={gitStatus}
|
||||||
toolTypes={toolTypes}
|
toolTypes={toolTypes}
|
||||||
|
currentBranch={currentBranch}
|
||||||
|
branches={branches}
|
||||||
|
hasRemote={Boolean(selectedRepo?.remote_url)}
|
||||||
onRepoChange={handleRepoChange}
|
onRepoChange={handleRepoChange}
|
||||||
|
onBranchChange={(branch) => {
|
||||||
|
setCurrentBranch(branch);
|
||||||
|
const newParams = new URLSearchParams(searchParams);
|
||||||
|
newParams.set("branch", branch);
|
||||||
|
setSearchParams(newParams);
|
||||||
|
}}
|
||||||
|
onRefresh={() => {
|
||||||
|
void loadBranches();
|
||||||
|
void loadGitStatus();
|
||||||
|
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||||
|
}}
|
||||||
onCommit={() => {
|
onCommit={() => {
|
||||||
void loadGitStatus();
|
void loadGitStatus();
|
||||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||||
|
|||||||
Reference in New Issue
Block a user