Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev

This commit is contained in:
Fusion
2026-05-24 19:59:49 +02:00
23 changed files with 3136 additions and 125 deletions
@@ -5,6 +5,7 @@ import type { Project } from "../types";
import { listRepositoryBranches, type GitRepository, type Branch } from "../api/git_repositories";
import type { ToolType } from "../api/tool_types";
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles";
interface CreateSessionFormProps {
projects: Project[];
@@ -46,6 +47,8 @@ export const CreateSessionForm = ({
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
const [branch, setBranch] = useState("main");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
const [branches, setBranches] = useState<Branch[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
@@ -71,6 +74,30 @@ export const CreateSessionForm = ({
void loadKeys();
}, [showCloneMode]);
// Load config profiles when tool type is selected
useEffect(() => {
const projectId = fixedProjectId || selectedProject;
if (!selectedToolType || !projectId) {
setConfigProfiles([]);
setSelectedConfigProfile("");
return;
}
const loadProfiles = async () => {
try {
const profiles = await listConfigProfiles(projectId, selectedToolType);
setConfigProfiles(profiles);
// Auto-select default if available
const defaultProfile = profiles.find((p) => p.is_default);
if (defaultProfile) {
setSelectedConfigProfile(defaultProfile.id);
}
} catch {
// ignore
}
};
void loadProfiles();
}, [selectedToolType, selectedProject, fixedProjectId]);
// Load branches when selected repo changes
useEffect(() => {
const projectId = fixedProjectId || selectedProject;
@@ -138,7 +165,8 @@ export const CreateSessionForm = ({
: undefined,
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
? newBranchName
: undefined
: undefined,
selectedConfigProfile || undefined
);
setProgress("Starting container...");
@@ -298,8 +326,26 @@ export const CreateSessionForm = ({
</label>
)}
{/* Step 4: Clone Mode & Branch */}
{showCloneMode && hasToolType && renderStep("Repository Access", 4, true, false,
{/* Step 4: Config Profile */}
{hasToolType && renderStep("Config Profile (optional)", 4, true, false,
<label className="form-field">
<select
value={selectedConfigProfile}
onChange={(e) => setSelectedConfigProfile(e.target.value)}
disabled={!hasToolType || isSubmitting}
>
<option value="">No profile (use tool defaults)</option>
{configProfiles.map((p) => (
<option key={p.id} value={p.id}>
{p.name} {p.is_default ? "(default)" : ""}
</option>
))}
</select>
</label>
)}
{/* Step 5: Clone Mode & Branch */}
{showCloneMode && hasToolType && renderStep("Repository Access", 5, true, false,
<div className="form-row">
<label className="form-field">
<div className="radio-group">
@@ -422,8 +468,8 @@ export const CreateSessionForm = ({
</div>
)}
{/* Step 5: Display Name */}
{hasToolType && renderStep("Display Name (optional)", 5, true, !!displayName,
{/* Step 6: Display Name */}
{hasToolType && renderStep("Display Name (optional)", 6, true, !!displayName,
<label className="form-field">
<input
type="text"
+131 -21
View File
@@ -13,6 +13,7 @@ import {
} from "../api/sessions";
import type { ToolType } from "../api/tool_types";
import { CreateSessionForm } from "./create-session-form";
import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
@@ -30,13 +31,18 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
const [loading, setLoading] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const [error, setError] = useState<string | null>(null);
// Stop confirmation
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
// Health check state
const [healthStatus, setHealthStatus] = useState<Record<string, { healthy: boolean; lastCheck: number }>>({});
// Config profile selection for start/restart
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
const [profileSelectInstanceId, setProfileSelectInstanceId] = useState<string | null>(null);
const [selectedProfileForAction, setSelectedProfileForAction] = useState("");
const loadInstances = useCallback(async () => {
setLoading(true);
try {
@@ -88,9 +94,20 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
await loadInstances();
};
const handleStart = async (instanceId: string) => {
const loadConfigProfiles = useCallback(async (toolTypeId: string) => {
try {
await startInstance(projectId, repoId, instanceId);
const profiles = await listConfigProfiles(projectId, toolTypeId);
setConfigProfiles(profiles);
} catch {
// ignore
}
}, [projectId]);
const handleStart = async (instanceId: string, configProfileId?: string) => {
try {
await startInstance(projectId, repoId, instanceId, configProfileId);
setProfileSelectInstanceId(null);
setSelectedProfileForAction("");
await loadInstances();
} catch {
setError("Failed to start instance");
@@ -107,9 +124,11 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
}
};
const handleRestart = async (instanceId: string) => {
const handleRestart = async (instanceId: string, configProfileId?: string) => {
try {
await restartInstance(projectId, repoId, instanceId);
await restartInstance(projectId, repoId, instanceId, configProfileId);
setProfileSelectInstanceId(null);
setSelectedProfileForAction("");
await loadInstances();
} catch {
setError("Failed to restart instance");
@@ -199,6 +218,13 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
</span>
)}
</div>
{instance.selected_config_profile_id && (
<div className="instance-profile">
<span className="badge">
Profile: {configProfiles.find((p) => p.id === instance.selected_config_profile_id)?.name || instance.selected_config_profile_id}
</span>
</div>
)}
</div>
<div className="instance-actions">
{instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && (
@@ -236,14 +262,57 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
</button>
)}
{instance.status !== "running" && (
<button
className="secondary-button small"
onClick={() => void handleStart(instance.id)}
type="button"
>
<Icon name="play" size="sm" />
Start
</button>
<>
{profileSelectInstanceId === instance.id ? (
<div className="inline-profile-select">
<select
value={selectedProfileForAction}
onChange={(e) => setSelectedProfileForAction(e.target.value)}
>
<option value="">Default (none)</option>
{configProfiles.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<button
className="primary-button small"
onClick={() => void handleStart(instance.id, selectedProfileForAction || undefined)}
type="button"
>
<Icon name="play" size="sm" />
Start
</button>
<button
className="ghost-button small"
onClick={() => {
setProfileSelectInstanceId(null);
setSelectedProfileForAction("");
}}
type="button"
>
Cancel
</button>
</div>
) : (
<button
className="secondary-button small"
onClick={() => {
const toolType = toolTypes.find((t) => t.id === instance.tool_type_id);
if (toolType) {
void loadConfigProfiles(toolType.id);
}
setProfileSelectInstanceId(instance.id);
setSelectedProfileForAction(instance.selected_config_profile_id || "");
}}
type="button"
>
<Icon name="play" size="sm" />
Start
</button>
)}
</>
)}
{instance.status === "running" && (
<>
@@ -274,13 +343,54 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
<Icon name="stop" size="sm" />
</button>
)}
<button
className="ghost-button small"
onClick={() => void handleRestart(instance.id)}
type="button"
>
<Icon name="refresh" size="sm" />
</button>
{profileSelectInstanceId === instance.id ? (
<div className="inline-profile-select">
<select
value={selectedProfileForAction}
onChange={(e) => setSelectedProfileForAction(e.target.value)}
>
<option value="">Default (none)</option>
{configProfiles.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<button
className="primary-button small"
onClick={() => void handleRestart(instance.id, selectedProfileForAction || undefined)}
type="button"
>
<Icon name="refresh" size="sm" />
Restart
</button>
<button
className="ghost-button small"
onClick={() => {
setProfileSelectInstanceId(null);
setSelectedProfileForAction("");
}}
type="button"
>
Cancel
</button>
</div>
) : (
<button
className="ghost-button small"
onClick={() => {
const toolType = toolTypes.find((t) => t.id === instance.tool_type_id);
if (toolType) {
void loadConfigProfiles(toolType.id);
}
setProfileSelectInstanceId(instance.id);
setSelectedProfileForAction(instance.selected_config_profile_id || "");
}}
type="button"
>
<Icon name="refresh" size="sm" />
</button>
)}
</>
)}
<button