ProcessCard.svelte raw
1 <script>
2 import { createEventDispatcher } from 'svelte';
3
4 export let process;
5 export let isLoading = false;
6
7 const dispatch = createEventDispatcher();
8
9 // Categories that are mutually exclusive (only one can be enabled)
10 const exclusiveCategories = ['database', 'acl'];
11
12 // Check if this process can be toggled (relay is always on)
13 $: canToggle = process.category !== 'relay';
14
15 // Check if this is in an exclusive category
16 $: isExclusive = exclusiveCategories.includes(process.category);
17
18 function getStatusColor(status) {
19 switch (status) {
20 case 'running': return 'var(--success)';
21 case 'stopped': return 'var(--muted-color)';
22 case 'disabled': return 'var(--muted-color)';
23 case 'crashed': return 'var(--error)';
24 default: return 'var(--muted-color)';
25 }
26 }
27
28 function getStatusIcon(status) {
29 switch (status) {
30 case 'running': return '●';
31 case 'stopped': return '○';
32 case 'disabled': return '◌';
33 case 'crashed': return '✗';
34 default: return '?';
35 }
36 }
37
38 function getCategoryLabel(category) {
39 switch (category) {
40 case 'database': return 'Database';
41 case 'acl': return 'Access Control';
42 case 'sync': return 'Sync Service';
43 case 'certs': return 'Certificates';
44 case 'relay': return 'Relay';
45 default: return category;
46 }
47 }
48
49 function handleStart() {
50 dispatch('start', { service: process.name });
51 }
52
53 function handleStop() {
54 dispatch('stop', { service: process.name });
55 }
56
57 function handleRestart() {
58 dispatch('restart', { service: process.name });
59 }
60
61 function handleToggleEnabled(event) {
62 const newEnabled = event.target.checked;
63 dispatch('toggle-enabled', {
64 service: process.name,
65 enabled: newEnabled,
66 category: process.category,
67 isExclusive: isExclusive
68 });
69 }
70
71 $: canStart = process.enabled && process.status !== 'running';
72 $: canStop = process.status === 'running';
73 $: canRestart = process.status === 'running';
74 </script>
75
76 <div class="process-card" class:disabled={!process.enabled}>
77 <div class="process-header">
78 <span class="status-indicator" style="color: {getStatusColor(process.status)}">
79 {getStatusIcon(process.status)}
80 </span>
81 <div class="name-section">
82 <span class="process-name">{process.name}</span>
83 <span class="category-badge" class:exclusive={isExclusive}>{getCategoryLabel(process.category)}</span>
84 </div>
85 {#if canToggle}
86 <label class="enable-toggle" title={process.enabled ? 'Disable' : 'Enable'}>
87 <input
88 type="checkbox"
89 checked={process.enabled}
90 on:change={handleToggleEnabled}
91 disabled={isLoading || process.status === 'running'}
92 />
93 <span class="toggle-slider"></span>
94 </label>
95 {:else}
96 <span class="badge required-badge">always on</span>
97 {/if}
98 </div>
99
100 <p class="description">{process.description}</p>
101
102 <div class="process-details">
103 <div class="detail-row">
104 <span class="label">Status:</span>
105 <span class="value" style="color: {getStatusColor(process.status)}">
106 {process.status}
107 </span>
108 </div>
109
110 {#if process.pid > 0}
111 <div class="detail-row">
112 <span class="label">PID:</span>
113 <span class="value">{process.pid}</span>
114 </div>
115 {/if}
116
117 {#if process.restarts > 0}
118 <div class="detail-row">
119 <span class="label">Restarts:</span>
120 <span class="value warning">{process.restarts}</span>
121 </div>
122 {/if}
123 </div>
124
125 <div class="process-actions">
126 {#if canStart}
127 <button class="action-btn start-btn" on:click={handleStart} disabled={isLoading} title="Start service">
128 ▶
129 </button>
130 {/if}
131 {#if canStop}
132 <button class="action-btn stop-btn" on:click={handleStop} disabled={isLoading} title="Stop service">
133 ■
134 </button>
135 {/if}
136 {#if canRestart}
137 <button class="action-btn restart-btn" on:click={handleRestart} disabled={isLoading} title="Restart service">
138 ↻
139 </button>
140 {/if}
141 {#if !process.enabled && !canStart && !canStop}
142 <span class="hint">Enable to start</span>
143 {/if}
144 </div>
145 </div>
146
147 <style>
148 .process-card {
149 background: var(--card-bg);
150 border: 1px solid var(--border-color);
151 border-radius: 8px;
152 padding: 16px;
153 display: flex;
154 flex-direction: column;
155 gap: 10px;
156 }
157
158 .process-card.disabled {
159 opacity: 0.6;
160 }
161
162 .process-header {
163 display: flex;
164 align-items: center;
165 gap: 8px;
166 }
167
168 .status-indicator {
169 font-size: 1.2rem;
170 flex-shrink: 0;
171 }
172
173 .name-section {
174 flex: 1;
175 display: flex;
176 flex-direction: column;
177 gap: 2px;
178 }
179
180 .process-name {
181 font-weight: 600;
182 font-size: 0.95rem;
183 color: var(--text-color);
184 }
185
186 .category-badge {
187 font-size: 0.65rem;
188 padding: 1px 4px;
189 border-radius: 3px;
190 text-transform: uppercase;
191 background: var(--border-color);
192 color: var(--muted-color);
193 width: fit-content;
194 }
195
196 .category-badge.exclusive {
197 background: var(--warning, #ff9800);
198 color: white;
199 opacity: 0.8;
200 }
201
202 .description {
203 font-size: 0.8rem;
204 color: var(--muted-color);
205 margin: 0;
206 line-height: 1.3;
207 }
208
209 .badge {
210 font-size: 0.65rem;
211 padding: 2px 6px;
212 border-radius: 4px;
213 text-transform: uppercase;
214 flex-shrink: 0;
215 }
216
217 .required-badge {
218 background: var(--text-color);
219 color: var(--card-bg);
220 opacity: 0.4;
221 }
222
223 .enable-toggle {
224 position: relative;
225 display: inline-block;
226 width: 36px;
227 height: 20px;
228 cursor: pointer;
229 flex-shrink: 0;
230 }
231
232 .enable-toggle input {
233 opacity: 0;
234 width: 0;
235 height: 0;
236 }
237
238 .toggle-slider {
239 position: absolute;
240 top: 0;
241 left: 0;
242 right: 0;
243 bottom: 0;
244 background-color: var(--muted-color);
245 border-radius: 20px;
246 transition: 0.2s;
247 }
248
249 .toggle-slider:before {
250 position: absolute;
251 content: "";
252 height: 14px;
253 width: 14px;
254 left: 3px;
255 bottom: 3px;
256 background-color: white;
257 border-radius: 50%;
258 transition: 0.2s;
259 }
260
261 .enable-toggle input:checked + .toggle-slider {
262 background-color: var(--success, #4caf50);
263 }
264
265 .enable-toggle input:checked + .toggle-slider:before {
266 transform: translateX(16px);
267 }
268
269 .enable-toggle input:disabled + .toggle-slider {
270 opacity: 0.5;
271 cursor: not-allowed;
272 }
273
274 .process-details {
275 display: flex;
276 flex-direction: column;
277 gap: 4px;
278 }
279
280 .detail-row {
281 display: flex;
282 justify-content: space-between;
283 font-size: 0.8rem;
284 }
285
286 .label {
287 color: var(--muted-color);
288 }
289
290 .value {
291 color: var(--text-color);
292 font-family: monospace;
293 }
294
295 .value.warning {
296 color: var(--warning);
297 }
298
299 .process-actions {
300 display: flex;
301 gap: 8px;
302 align-items: center;
303 margin-top: 4px;
304 padding-top: 10px;
305 border-top: 1px solid var(--border-color);
306 }
307
308 .action-btn {
309 width: 32px;
310 height: 32px;
311 border-radius: 4px;
312 border: none;
313 cursor: pointer;
314 font-size: 1rem;
315 display: flex;
316 align-items: center;
317 justify-content: center;
318 transition: opacity 0.2s, transform 0.1s;
319 }
320
321 .action-btn:hover:not(:disabled) {
322 transform: scale(1.05);
323 }
324
325 .action-btn:disabled {
326 opacity: 0.5;
327 cursor: not-allowed;
328 }
329
330 .start-btn {
331 background: var(--success, #4caf50);
332 color: white;
333 }
334
335 .stop-btn {
336 background: var(--error, #f44336);
337 color: white;
338 }
339
340 .restart-btn {
341 background: var(--warning, #ff9800);
342 color: white;
343 }
344
345 .hint {
346 font-size: 0.75rem;
347 color: var(--muted-color);
348 font-style: italic;
349 }
350 </style>
351