vue · 12212 bytes Raw Blame History
1 <script setup lang="ts">
2 import { ref, onMounted } from "vue";
3 import { invoke } from "@tauri-apps/api/core";
4
5 interface StorageConfig {
6 folder_path: string;
7 max_size_bytes: number;
8 warn_at_percent: number;
9 auto_cleanup_enabled: boolean;
10 }
11
12 interface FolderValidation {
13 is_valid: boolean;
14 error_message?: string;
15 available_space_bytes: number;
16 recommended_max_size: number;
17 warnings: string[];
18 }
19
20 interface StorageStatus {
21 used_bytes: number;
22 total_allocated_bytes: number;
23 usage_percent: number;
24 chunks_count: number;
25 disk_available_bytes: number;
26 needs_attention: boolean;
27 }
28
29 const currentStep = ref(1);
30 const suggestions = ref<string[]>([]);
31 const selectedFolder = ref("");
32 const validation = ref<FolderValidation | null>(null);
33 const maxSizeGB = ref(10);
34 const isCreating = ref(false);
35 const status = ref<StorageStatus | null>(null);
36
37 const formatBytes = (bytes: number): string => {
38 const units = ["B", "KB", "MB", "GB", "TB"];
39 let size = bytes;
40 let unitIndex = 0;
41
42 while (size >= 1024 && unitIndex < units.length - 1) {
43 size /= 1024;
44 unitIndex++;
45 }
46
47 return unitIndex === 0
48 ? `${Math.floor(size)} ${units[unitIndex]}`
49 : `${size.toFixed(2)} ${units[unitIndex]}`;
50 };
51
52 const loadSuggestions = async () => {
53 try {
54 suggestions.value = await invoke("get_default_storage_suggestions");
55 if (suggestions.value.length > 0) {
56 selectedFolder.value = suggestions.value[0];
57 }
58 } catch (error) {
59 console.error("Failed to load suggestions:", error);
60 }
61 };
62
63 const validateFolder = async () => {
64 if (!selectedFolder.value) return;
65
66 try {
67 validation.value = await invoke("validate_storage_folder", {
68 folderPath: selectedFolder.value
69 });
70 if (validation.value?.is_valid) {
71 maxSizeGB.value = Math.floor(validation.value.recommended_max_size / (1024 * 1024 * 1024));
72 }
73 } catch (error) {
74 console.error("Failed to validate folder:", error);
75 }
76 };
77
78 const createStorage = async () => {
79 if (!selectedFolder.value || !validation.value?.is_valid) return;
80
81 isCreating.value = true;
82 try {
83 const config: StorageConfig = {
84 folder_path: selectedFolder.value,
85 max_size_bytes: maxSizeGB.value * 1024 * 1024 * 1024,
86 warn_at_percent: 80.0,
87 auto_cleanup_enabled: true
88 };
89
90 await invoke("create_storage_folder", { config });
91 currentStep.value = 3;
92 loadStatus();
93 } catch (error) {
94 console.error("Failed to create storage:", error);
95 } finally {
96 isCreating.value = false;
97 }
98 };
99
100 const loadStatus = async () => {
101 if (!selectedFolder.value) return;
102
103 try {
104 status.value = await invoke("get_storage_status", {
105 folderPath: selectedFolder.value
106 });
107 } catch (error) {
108 console.error("Failed to load status:", error);
109 }
110 };
111
112 onMounted(() => {
113 loadSuggestions();
114 });
115 </script>
116
117 <template>
118 <main class="container">
119 <h1>ZephyrFS Storage Node Setup</h1>
120
121 <!-- Step 1: Folder Selection -->
122 <div v-if="currentStep === 1" class="step">
123 <h2>Step 1: Choose Storage Location</h2>
124 <p>Select a safe folder to store encrypted chunks from other users.</p>
125
126 <div class="suggestions">
127 <h3>Recommended locations:</h3>
128 <div class="folder-options">
129 <label v-for="suggestion in suggestions" :key="suggestion" class="folder-option">
130 <input
131 type="radio"
132 v-model="selectedFolder"
133 :value="suggestion"
134 @change="validateFolder"
135 />
136 <span>{{ suggestion }}</span>
137 </label>
138 </div>
139 </div>
140
141 <div class="custom-folder">
142 <label>
143 Custom folder path:
144 <input
145 type="text"
146 v-model="selectedFolder"
147 @input="validateFolder"
148 placeholder="Enter custom folder path..."
149 />
150 </label>
151 </div>
152
153 <div v-if="validation" class="validation">
154 <div v-if="validation.is_valid" class="valid">
155 Valid location
156 <p>Available space: {{ formatBytes(validation.available_space_bytes) }}</p>
157 <p>Recommended max: {{ formatBytes(validation.recommended_max_size) }}</p>
158 </div>
159 <div v-else class="invalid">
160 {{ validation.error_message }}
161 </div>
162
163 <div v-if="validation.warnings.length > 0" class="warnings">
164 <h4>Warnings:</h4>
165 <ul>
166 <li v-for="warning in validation.warnings" :key="warning">{{ warning }}</li>
167 </ul>
168 </div>
169 </div>
170
171 <button
172 @click="currentStep = 2"
173 :disabled="!validation?.is_valid"
174 class="next-btn"
175 >
176 Next: Configure Storage
177 </button>
178 </div>
179
180 <!-- Step 2: Storage Configuration -->
181 <div v-if="currentStep === 2" class="step">
182 <h2>Step 2: Configure Storage Allocation</h2>
183 <p>Set how much space to dedicate to ZephyrFS storage.</p>
184
185 <div class="config-section">
186 <label>
187 Maximum Storage (GB):
188 <input
189 type="number"
190 v-model="maxSizeGB"
191 min="1"
192 :max="Math.floor((validation?.recommended_max_size || 0) / (1024 * 1024 * 1024))"
193 />
194 </label>
195 <p class="size-info">{{ formatBytes(maxSizeGB * 1024 * 1024 * 1024) }}</p>
196 </div>
197
198 <div class="safety-info">
199 <h3>Safety Guarantees:</h3>
200 <ul>
201 <li> All stored data is encrypted and unreadable</li>
202 <li> No personal information is stored</li>
203 <li> You can safely delete this folder anytime</li>
204 <li> Storage is limited to your specified amount</li>
205 </ul>
206 </div>
207
208 <div class="buttons">
209 <button @click="currentStep = 1" class="back-btn">Back</button>
210 <button
211 @click="createStorage"
212 :disabled="isCreating"
213 class="create-btn"
214 >
215 {{ isCreating ? "Creating..." : "Create Storage Node" }}
216 </button>
217 </div>
218 </div>
219
220 <!-- Step 3: Success & Status -->
221 <div v-if="currentStep === 3" class="step">
222 <h2> Storage Node Ready!</h2>
223 <p>Your ZephyrFS storage node has been created successfully.</p>
224
225 <div v-if="status" class="status-panel">
226 <h3>Current Status:</h3>
227 <div class="status-grid">
228 <div class="status-item">
229 <span class="label">Used Space:</span>
230 <span class="value">{{ formatBytes(status.used_bytes) }}</span>
231 </div>
232 <div class="status-item">
233 <span class="label">Total Allocated:</span>
234 <span class="value">{{ formatBytes(status.total_allocated_bytes) }}</span>
235 </div>
236 <div class="status-item">
237 <span class="label">Usage:</span>
238 <span class="value">{{ status.usage_percent.toFixed(1) }}%</span>
239 </div>
240 <div class="status-item">
241 <span class="label">Chunks Stored:</span>
242 <span class="value">{{ status.chunks_count }}</span>
243 </div>
244 </div>
245
246 <div class="progress-bar">
247 <div
248 class="progress-fill"
249 :style="{ width: `${Math.min(status.usage_percent, 100)}%` }"
250 ></div>
251 </div>
252 </div>
253
254 <div class="next-steps">
255 <h3>What's Next:</h3>
256 <p>Your storage node will now participate in the ZephyrFS network, safely storing encrypted chunks from other users.</p>
257 <p>Check the system tray for real-time status updates.</p>
258 </div>
259 </div>
260 </main>
261 </template>
262
263 <style scoped>
264 .step {
265 max-width: 600px;
266 margin: 0 auto;
267 padding: 2rem;
268 background: #ffffff;
269 border-radius: 12px;
270 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
271 }
272
273 .suggestions {
274 margin: 1.5rem 0;
275 }
276
277 .folder-options {
278 display: flex;
279 flex-direction: column;
280 gap: 0.5rem;
281 margin: 1rem 0;
282 }
283
284 .folder-option {
285 display: flex;
286 align-items: center;
287 gap: 0.5rem;
288 padding: 0.5rem;
289 border: 1px solid #e0e0e0;
290 border-radius: 6px;
291 cursor: pointer;
292 transition: background-color 0.2s;
293 }
294
295 .folder-option:hover {
296 background-color: #f5f5f5;
297 }
298
299 .custom-folder {
300 margin: 1.5rem 0;
301 }
302
303 .custom-folder input {
304 width: 100%;
305 margin-top: 0.5rem;
306 }
307
308 .validation {
309 margin: 1.5rem 0;
310 padding: 1rem;
311 border-radius: 6px;
312 }
313
314 .valid {
315 background-color: #d4edda;
316 color: #155724;
317 border: 1px solid #c3e6cb;
318 }
319
320 .invalid {
321 background-color: #f8d7da;
322 color: #721c24;
323 border: 1px solid #f5c6cb;
324 }
325
326 .warnings {
327 margin-top: 1rem;
328 padding: 1rem;
329 background-color: #fff3cd;
330 color: #856404;
331 border: 1px solid #ffeaa7;
332 border-radius: 6px;
333 }
334
335 .config-section {
336 margin: 1.5rem 0;
337 }
338
339 .size-info {
340 font-size: 0.9rem;
341 color: #666;
342 margin-top: 0.5rem;
343 }
344
345 .safety-info {
346 margin: 2rem 0;
347 padding: 1.5rem;
348 background-color: #e8f5e8;
349 border-radius: 8px;
350 }
351
352 .safety-info ul {
353 list-style: none;
354 padding: 0;
355 }
356
357 .safety-info li {
358 margin: 0.5rem 0;
359 padding-left: 1rem;
360 }
361
362 .buttons {
363 display: flex;
364 gap: 1rem;
365 justify-content: flex-end;
366 margin-top: 2rem;
367 }
368
369 .next-btn, .create-btn {
370 background-color: #007bff;
371 color: white;
372 }
373
374 .back-btn {
375 background-color: #6c757d;
376 color: white;
377 }
378
379 .next-btn:disabled, .create-btn:disabled {
380 background-color: #cccccc;
381 cursor: not-allowed;
382 }
383
384 .status-panel {
385 margin: 2rem 0;
386 padding: 1.5rem;
387 background-color: #f8f9fa;
388 border-radius: 8px;
389 }
390
391 .status-grid {
392 display: grid;
393 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
394 gap: 1rem;
395 margin: 1rem 0;
396 }
397
398 .status-item {
399 display: flex;
400 justify-content: space-between;
401 padding: 0.5rem;
402 background-color: white;
403 border-radius: 4px;
404 }
405
406 .label {
407 font-weight: 500;
408 }
409
410 .value {
411 font-weight: 600;
412 color: #007bff;
413 }
414
415 .progress-bar {
416 width: 100%;
417 height: 8px;
418 background-color: #e0e0e0;
419 border-radius: 4px;
420 margin: 1rem 0;
421 overflow: hidden;
422 }
423
424 .progress-fill {
425 height: 100%;
426 background-color: #28a745;
427 transition: width 0.3s ease;
428 }
429
430 .next-steps {
431 margin: 2rem 0;
432 padding: 1.5rem;
433 background-color: #e8f4fd;
434 border-radius: 8px;
435 }
436
437 @media (prefers-color-scheme: dark) {
438 .step {
439 background-color: #2d2d2d;
440 color: #f6f6f6;
441 }
442
443 .folder-option {
444 border-color: #555;
445 background-color: #333;
446 }
447
448 .folder-option:hover {
449 background-color: #444;
450 }
451
452 .status-panel {
453 background-color: #333;
454 }
455
456 .status-item {
457 background-color: #444;
458 }
459
460 .safety-info {
461 background-color: #2d4a2d;
462 }
463
464 .next-steps {
465 background-color: #2d3a4a;
466 }
467 }
468 </style>
469 <style>
470 :root {
471 font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
472 font-size: 16px;
473 line-height: 24px;
474 font-weight: 400;
475
476 color: #0f0f0f;
477 background-color: #f6f6f6;
478
479 font-synthesis: none;
480 text-rendering: optimizeLegibility;
481 -webkit-font-smoothing: antialiased;
482 -moz-osx-font-smoothing: grayscale;
483 -webkit-text-size-adjust: 100%;
484 }
485
486 .container {
487 margin: 0;
488 padding-top: 10vh;
489 display: flex;
490 flex-direction: column;
491 justify-content: center;
492 text-align: center;
493 }
494
495 .logo {
496 height: 6em;
497 padding: 1.5em;
498 will-change: filter;
499 transition: 0.75s;
500 }
501
502 .logo.tauri:hover {
503 filter: drop-shadow(0 0 2em #24c8db);
504 }
505
506 .row {
507 display: flex;
508 justify-content: center;
509 }
510
511 a {
512 font-weight: 500;
513 color: #646cff;
514 text-decoration: inherit;
515 }
516
517 a:hover {
518 color: #535bf2;
519 }
520
521 h1 {
522 text-align: center;
523 }
524
525 input,
526 button {
527 border-radius: 8px;
528 border: 1px solid transparent;
529 padding: 0.6em 1.2em;
530 font-size: 1em;
531 font-weight: 500;
532 font-family: inherit;
533 color: #0f0f0f;
534 background-color: #ffffff;
535 transition: border-color 0.25s;
536 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
537 }
538
539 button {
540 cursor: pointer;
541 }
542
543 button:hover {
544 border-color: #396cd8;
545 }
546 button:active {
547 border-color: #396cd8;
548 background-color: #e8e8e8;
549 }
550
551 input,
552 button {
553 outline: none;
554 }
555
556 #greet-input {
557 margin-right: 5px;
558 }
559
560 @media (prefers-color-scheme: dark) {
561 :root {
562 color: #f6f6f6;
563 background-color: #2f2f2f;
564 }
565
566 a:hover {
567 color: #24c8db;
568 }
569
570 input,
571 button {
572 color: #ffffff;
573 background-color: #0f0f0f98;
574 }
575 button:active {
576 background-color: #0f0f0f69;
577 }
578 }
579
580 </style>