File size: 4,583 Bytes
6a7089a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { useEffect, useMemo, useState } from "react";
import { Button, Input, Modal } from "../atoms";
import { useAppStore } from "../../stores/useAppStore";
import * as api from "../../services/api";
import type { LaunchInstanceRequest, Profile } from "../../generated/types";

interface Props {
  open: boolean;
  profile: Profile | null;
  onClose: () => void;
}

export default function StartInstanceModal({ open, profile, onClose }: Props) {
  const { setInstances } = useAppStore();
  const [port, setPort] = useState("");
  const [headless, setHeadless] = useState(false);
  const [launchError, setLaunchError] = useState("");
  const [launchLoading, setLaunchLoading] = useState(false);
  const [copyFeedback, setCopyFeedback] = useState("");

  useEffect(() => {
    if (open) {
      setLaunchError("");
      setCopyFeedback("");
      return;
    }

    setPort("");
    setHeadless(false);
    setLaunchError("");
    setLaunchLoading(false);
    setCopyFeedback("");
  }, [open, profile?.id, profile?.name]);

  const launchCommand = useMemo(() => {
    if (!profile) return "";

    const payload: LaunchInstanceRequest = {
      profileId: profile.id || profile.name,
      mode: headless ? undefined : "headed",
      port: port.trim() || undefined,
    };

    return `curl -X POST http://localhost:9867/instances/start -H "Content-Type: application/json" -d '${JSON.stringify(payload)}'`;
  }, [headless, port, profile]);

  const handleLaunch = async () => {
    if (!profile || launchLoading) return;

    setLaunchError("");
    setLaunchLoading(true);

    try {
      const payload: LaunchInstanceRequest = {
        name: profile.name,
        port: port.trim() || undefined,
        mode: headless ? undefined : "headed",
      };

      await api.launchInstance(payload);
      const updated = await api.fetchInstances();
      setInstances(updated);
      onClose();
    } catch (e) {
      console.error("Launch failed:", e);
      const msg = e instanceof Error ? e.message : "Failed to launch instance";
      setLaunchError(msg);
    } finally {
      setLaunchLoading(false);
    }
  };

  const handleCopyCommand = async () => {
    try {
      await navigator.clipboard.writeText(launchCommand);
      setCopyFeedback("Copied!");
      setTimeout(() => setCopyFeedback(""), 2000);
    } catch {
      setCopyFeedback("Failed to copy");
      setTimeout(() => setCopyFeedback(""), 2000);
    }
  };

  return (
    <Modal
      open={open}
      onClose={onClose}
      title="🖥️ Start Profile"
      actions={
        <>
          <Button
            variant="secondary"
            disabled={launchLoading}
            onClick={onClose}
          >
            Cancel
          </Button>
          <Button
            variant="primary"
            onClick={handleLaunch}
            loading={launchLoading}
          >
            Start
          </Button>
        </>
      }
    >
      <div className="flex flex-col gap-4">
        {launchError && (
          <div className="rounded border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
            {launchError}
          </div>
        )}
        <Input
          label="Port"
          placeholder="Auto-select from configured range"
          value={port}
          onChange={(e) => setPort(e.target.value)}
        />
        <p className="-mt-2 text-xs text-text-muted">
          Leave blank to auto-select a free port from the configured instance
          port range.
        </p>
        <label className="flex items-center gap-2 text-sm text-text-secondary">
          <input
            type="checkbox"
            checked={headless}
            onChange={(e) => setHeadless(e.target.checked)}
            className="h-4 w-4"
          />
          Headless (best for Docker/VPS)
        </label>

        <div>
          <label className="mb-1 block text-xs text-text-muted">
            Direct launch command (backup)
          </label>
          <textarea
            readOnly
            value={launchCommand}
            className="h-20 w-full resize-none rounded border border-border-subtle bg-bg-elevated px-3 py-2 font-mono text-xs text-text-secondary"
          />
          <div className="mt-2 flex items-center gap-2">
            <Button size="sm" variant="secondary" onClick={handleCopyCommand}>
              Copy Command
            </Button>
            {copyFeedback && (
              <span className="text-xs text-success">{copyFeedback}</span>
            )}
          </div>
        </div>
      </div>
    </Modal>
  );
}