Skip to content

Commit 5adeb78

Browse files
committed
release: v2.5.0 - performance optimization + loading animations
1 parent 1d27817 commit 5adeb78

File tree

11 files changed

+920
-134
lines changed

11 files changed

+920
-134
lines changed

CHANGELOG.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,87 @@
22

33
All notable changes to SurfManager will be documented in this file.
44

5+
## [2.5.0] - 2026-02-07
6+
7+
### Summary
8+
9+
Major performance optimization release with loading animations. Backup/restore operations are now 3-5x faster, session listing is instant, and all action buttons now show loading feedback.
10+
11+
### ✨ New Features
12+
13+
**Loading Animations**
14+
- All action buttons now display a spinning loader during operations
15+
- Dynamic button text shows current state (e.g., "Resetting...", "Creating...", "Deleting...")
16+
- Buttons are disabled during loading to prevent double-clicks
17+
18+
**Reset Tab Loading States**
19+
- Reset button: spinner + "Resetting..." during reset operation
20+
- Addons button: spinner + "Deleting..." during addon folder deletion
21+
- New ID button: spinner + "Generating..." during machine ID generation
22+
- Kill button: spinner + "Stopping..." during app termination
23+
- Launch button: spinner + "Launching..." during app launch
24+
25+
**Sessions Tab Loading States**
26+
- Create Backup button: spinner + "Creating..." during backup creation
27+
- Kill and Continue button: spinner + "Processing..." during kill + backup
28+
- Restore Session (context menu): spinner + "Restoring..." during restore
29+
- Delete button (per row): spinner + "Deleting..." during session deletion
30+
31+
**Config Tab Loading States**
32+
- Save button: spinner + "Saving..." during app configuration save
33+
- Delete button: spinner during app deletion
34+
- Active/Inactive toggle: spinner during status change
35+
36+
### 🚀 Performance Improvements
37+
38+
**Parallel File Copying**
39+
- Backup and restore now use a worker pool with all available CPU cores
40+
- Each worker uses optimized 4MB I/O buffers for maximum throughput
41+
- File operations run concurrently instead of sequentially
42+
43+
**Streaming Hash Computation**
44+
- Files are hashed while being copied using `io.TeeReader`
45+
- Eliminates the previous double-read pattern (copy then hash)
46+
- Reduces I/O by 50% during backup operations
47+
48+
**Cached Metadata**
49+
- Session size and file count now stored in `.backup_meta.json`
50+
- Session listing no longer walks directory trees to calculate size
51+
- Session list loads instantly regardless of backup size
52+
53+
**Lazy Hash Verification**
54+
- Hash verification removed from session listing (was blocking UI)
55+
- New on-demand `VerifySessionIntegrity` API for explicit integrity checks
56+
- Corrupted status no longer computed at list time
57+
58+
### ✨ New Features
59+
60+
**Backend**
61+
- Added `VerifySessionIntegrity(appKey, sessionName)` API for on-demand backup verification
62+
- Enhanced metadata schema with `hash_version`, `size`, and `file_count` fields
63+
- New v2 hash algorithm matching streaming computation
64+
65+
### 📝 Technical Changes
66+
67+
**New Types**
68+
- `CopyJob` - represents a file copy operation with source, destination, and relative path
69+
- `FileCopyResult` - holds copy result including size, hash, and error status
70+
- `BackupMetadata` - full metadata structure with cached values
71+
72+
**New Functions**
73+
- `copyFileStreaming()` - copies files while computing SHA256 in single pass
74+
- `copyWithWorkerPool()` - parallel file copying with worker pool
75+
- `collectCopyJobs*()` - job collection helpers for items and addons
76+
- `computeHashFromResults()` - aggregate hash from sorted file results
77+
- `readBackupMetadataFull()` - reads full metadata including cached size
78+
- `computeBackupHashV2()` - v2 hash algorithm for new backups
79+
80+
### 🔄 Backwards Compatibility
81+
82+
- Old backups without cached size fall back to directory walk
83+
- Old hash format (v1) automatically detected and verified correctly
84+
- No migration required - old backups work seamlessly
85+
586
## [2.2.1] - 2026-02-05
687

788
### Summary (vs 2.2.0)

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# SurfManager v2.2.1
1+
# SurfManager v2.5.0
22

33
> **Advanced Session & Data Manager for Development Tools**
44
5-
[![Version](https://img.shields.io/badge/version-2.2.1-brightgreen.svg)](https://github.com/risunCode/SurfManager)
5+
[![Version](https://img.shields.io/badge/version-2.5.0-brightgreen.svg)](https://github.com/risunCode/SurfManager)
66
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-blue.svg)](https://github.com/risunCode/SurfManager)
77
[![Go](https://img.shields.io/badge/go-1.22+-00ADD8.svg)](https://golang.org/)
88
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -55,6 +55,14 @@ Perfect for developers who need to:
5555
| **📝 Custom App Config** | VSCode preset or fully custom backup items |
5656
| **✏️ Edit App Config** | Edit existing app configurations via UI |
5757

58+
### 🚀 What's New in v2.5.0
59+
60+
- **3-5x Faster Backup/Restore** - Parallel file copying using all CPU cores
61+
- **Instant Session Listing** - Cached metadata eliminates slow directory scans
62+
- **Streaming Hash Computation** - Files hashed while copying (no double reads)
63+
- **On-Demand Integrity Verification** - Lazy verification via new API
64+
- **4MB I/O Buffers** - Optimized for high-throughput file operations
65+
5866
### 🚀 What's New in v2.0
5967

6068
- **Complete Rewrite** - Go + Wails + Svelte (from Python + PyQt6)
@@ -390,7 +398,7 @@ SurfManager is open-source under the MIT License.
390398

391399
<div align="center">
392400

393-
**SurfManager v2.0**
401+
**SurfManager v2.5.0**
394402

395403
*Making development workflows smoother, one session at a time*
396404

app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,12 @@ func (a *App) CheckSessionHasAddons(appKey, sessionName string) bool {
12891289
return a.backup.SessionHasAddons(appKey, sessionName)
12901290
}
12911291

1292+
// VerifySessionIntegrity verifies a backup session's integrity on-demand.
1293+
// Returns true if the backup is valid, false if corrupted.
1294+
func (a *App) VerifySessionIntegrity(appKey, sessionName string) (bool, error) {
1295+
return a.backup.VerifySessionIntegrity(appKey, sessionName)
1296+
}
1297+
12921298
// generateUUID generates a simple UUID
12931299
func generateUUID() string {
12941300
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "frontend",
33
"private": true,
4-
"version": "2.2.1",
4+
"version": "2.5.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

frontend/src/lib/ConfigTab.svelte

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script>
22
import { onMount } from 'svelte';
3-
import { Plus, Search, Edit, Trash2, FolderOpen, Save, X, Check, RefreshCw, ToggleLeft, FileJson } from 'lucide-svelte';
3+
import { Plus, Search, Edit, Trash2, FolderOpen, Save, X, Check, RefreshCw, ToggleLeft, FileJson, Loader2 } from 'lucide-svelte';
44
import { CheckAppInstalled, GetApp, GetApps, OpenConfigFolder, SaveApp, DeleteApp, SelectExeFromLocalPrograms, SelectFolderFromHome, SelectFolderFromRoaming, SelectFolderFromLocalPrograms, ToggleApp, GetPlatformInfo } from '../../wailsjs/go/main/App.js';
55
import { confirm } from './ConfirmModal.svelte';
66
import { toast } from './Toast.svelte';
@@ -12,6 +12,11 @@
1212
let apps = [];
1313
let loading = false;
1414
let platformInfo = {};
15+
16+
// Loading states
17+
let loadingSave = false;
18+
let loadingDelete = {}; // { appKey: boolean }
19+
let loadingToggle = {}; // { appKey: boolean }
1520
1621
// Context menu state
1722
let contextMenu = { show: false, x: 0, y: 0, app: null };
@@ -86,27 +91,37 @@
8691
}
8792
8893
async function handleToggle(app) {
94+
if (loadingToggle[app.app_name]) return;
95+
96+
loadingToggle = { ...loadingToggle, [app.app_name]: true };
8997
try {
9098
await ToggleApp(app.app_name);
9199
log(`Toggled ${app.display_name}: ${app.active ? 'Inactive' : 'Active'}`);
92100
await loadApps();
93101
} catch (e) {
94102
log(`Error: ${e}`);
103+
} finally {
104+
loadingToggle = { ...loadingToggle, [app.app_name]: false };
95105
}
96106
}
97107
98108
async function handleDelete(app) {
109+
if (loadingDelete[app.app_name]) return;
110+
99111
if ($settings.confirmBeforeDelete) {
100112
const confirmed = await confirm.delete(app.display_name);
101113
if (!confirmed) return;
102114
}
103-
115+
116+
loadingDelete = { ...loadingDelete, [app.app_name]: true };
104117
try {
105118
await DeleteApp(app.app_name);
106119
log(`Deleted: ${app.display_name}`);
107120
await loadApps();
108121
} catch (e) {
109122
log(`Error: ${e}`);
123+
} finally {
124+
loadingDelete = { ...loadingDelete, [app.app_name]: false };
110125
}
111126
}
112127
@@ -319,6 +334,8 @@
319334
}
320335
321336
async function saveNewApp() {
337+
if (loadingSave) return;
338+
322339
if (!newApp.app_name || !newApp.exe_path || !newApp.data_path) {
323340
alert('Please fill in all required fields');
324341
return;
@@ -362,14 +379,19 @@
362379
addon_backup_paths: newApp.addon_paths
363380
};
364381
382+
loadingSave = true;
365383
try {
366384
await SaveApp(config);
367385
log(`${dialogMode === 'edit' ? 'Updated' : 'Added'}: ${config.display_name}`);
386+
toast.success(`${dialogMode === 'edit' ? 'Updated' : 'Added'} ${config.display_name}`, 3000);
368387
showAddDialog = false;
369388
resetNewApp();
370389
await loadApps();
371390
} catch (e) {
372391
log(`Error: ${e}`);
392+
toast.error(`Failed to save: ${e}`, 5000);
393+
} finally {
394+
loadingSave = false;
373395
}
374396
}
375397
@@ -501,12 +523,16 @@
501523
<td class="p-3">
502524
<div class="flex items-center gap-2">
503525
<button
504-
class="px-3 py-1 rounded text-xs font-medium transition-all
505-
{app.active
506-
? 'bg-[var(--success)]/20 text-[var(--success)] hover:bg-[var(--success)]/30'
526+
class="px-3 py-1 rounded text-xs font-medium transition-all flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed
527+
{app.active
528+
? 'bg-[var(--success)]/20 text-[var(--success)] hover:bg-[var(--success)]/30'
507529
: 'bg-[var(--bg-hover)] text-[var(--text-muted)] hover:bg-[var(--border)]'}"
508530
on:click={() => handleToggle(app)}
531+
disabled={loadingToggle[app.app_name]}
509532
>
533+
{#if loadingToggle[app.app_name]}
534+
<Loader2 size={12} class="animate-spin" />
535+
{/if}
510536
{app.active ? 'Active' : 'Inactive'}
511537
</button>
512538
<button
@@ -517,11 +543,16 @@
517543
<Edit size={14} />
518544
</button>
519545
<button
520-
class="p-1.5 rounded text-[var(--text-secondary)] hover:text-[var(--danger)] hover:bg-[var(--bg-hover)] transition-colors"
546+
class="p-1.5 rounded text-[var(--text-secondary)] hover:text-[var(--danger)] hover:bg-[var(--bg-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
521547
on:click={() => handleDelete(app)}
522548
title="Delete"
549+
disabled={loadingDelete[app.app_name]}
523550
>
524-
<Trash2 size={14} />
551+
{#if loadingDelete[app.app_name]}
552+
<Loader2 size={14} class="animate-spin" />
553+
{:else}
554+
<Trash2 size={14} />
555+
{/if}
525556
</button>
526557
</div>
527558
</td>
@@ -846,17 +877,24 @@
846877
</div>
847878
848879
<div class="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--border)]">
849-
<button
850-
class="px-4 py-2 rounded-lg font-medium bg-[var(--bg-hover)] hover:bg-[var(--border)] border border-[var(--border)] text-[var(--text-secondary)] transition-all"
880+
<button
881+
class="px-4 py-2 rounded-lg font-medium bg-[var(--bg-hover)] hover:bg-[var(--border)] border border-[var(--border)] text-[var(--text-secondary)] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
851882
on:click={() => { showAddDialog = false; resetNewApp(); }}
883+
disabled={loadingSave}
852884
>
853885
Cancel
854886
</button>
855-
<button
856-
class="px-4 py-2 rounded-lg font-medium bg-[var(--primary)] hover:bg-[var(--primary-light)] hover:text-black text-white transition-all"
887+
<button
888+
class="px-4 py-2 rounded-lg font-medium bg-[var(--primary)] hover:bg-[var(--primary-light)] hover:text-black text-white transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
857889
on:click={saveNewApp}
890+
disabled={loadingSave}
858891
>
859-
Save
892+
{#if loadingSave}
893+
<Loader2 size={16} class="animate-spin" />
894+
Saving...
895+
{:else}
896+
Save
897+
{/if}
860898
</button>
861899
</div>
862900
</div>

0 commit comments

Comments
 (0)