Commit graph

76 commits

Author SHA1 Message Date
1ecaefe1d8 feat: async analysis with polling + nested archive extraction
- Backend /api/v1/analyze now returns 202 with analysis_id immediately
- New GET /api/v1/analyze/{id} for polling analysis status
- Background thread handles extraction, disk discovery, OS/EFI detection
- Nested archive extraction: handles chained zips and split zips (.z01-.zNN)
- Frontend polls /session/analyze/status/{id} every 2s until complete
- SCP page now has complete confirm form flow with job polling
2026-07-26 00:13:14 +00:00
1e14d96052 docs: update README with current directory layout, install script download link, SCP/clone features, and session isolation 2026-07-25 18:17:09 +00:00
f1978008bd fix: use relative paths for STAGING_ROOT and LOG_DIR so backend works on host regardless of install location 2026-07-25 17:50:13 +00:00
f7c6527d3d chore: add AGENTS.md, move install.sh to root, rename frontend install.sh → setup.sh 2026-07-25 17:40:59 +00:00
root
ef2d74a62b fix: remove qemu-utils from installer + configurable storage path
1. Removed qemu-utils from required packages — Proxmox ships its own
   qemu-img and installing Debian's version breaks VMs. Added preflight
   check: if qemu-img missing, exits with clear error message.

2. Installer now prompts for host storage path (default:
   /mnt/pve/hosted-thin). Uses it for the LXC bind mount mp0.
   The LXC always sees /mnt/converter internally — no symlinks needed.
2026-07-25 11:23:26 +00:00
b33ab1605e fix: auto-cleanup staging files to minimize disk usage
Deletes intermediate files as conversion progresses:
- Archive (.7z/.zip) after extraction
- Source VMDK after qemu-img convert
- Local QCOW2 after qm disk import

Reduces peak storage from zip+VMDK+QCOW2 to just the largest
single file — critical for the 94 GB rootfs with 87 GB images.
2026-07-24 10:06:12 +00:00
a7ab9243c0 feat: SCP probes remote file size via SSH before pull
Runs 'ssh stat -c%s' or 'ls -l' to get total bytes before starting
SCP transfer. Progress bar now shows real percentage and ETA
instead of indeterminate spinner.
2026-07-24 09:48:43 +00:00
cd5f31505a fix: capture SCP stderr for better error messages 2026-07-24 09:43:19 +00:00
6e161a6105 fix: STAGING → _staging() in SCP disk check 2026-07-24 09:17:54 +00:00
6d4066784d ux: show exact auto-shrink target + warning about complex layouts 2026-07-23 13:04:00 +00:00
cd8d86a98c fix: auto-shrink to 10% of virtual size, min 20 GB
Replaces flat 30 GB default with proportional sizing:
- 500 GB → 50 GB, 200 GB → 20 GB, 80 GB → 20 GB
- Less aggressive for large disks, safer for GRUB boot
- Update UI hint and README to reflect new rule
2026-07-23 12:45:32 +00:00
778e929985 refactor: remove browser file upload — download + SCP only
- Removed UploadFile handling, upload-raw endpoint, source type selector
- index.html: single URL input, 'Download & Analyse' button
- session_upload simplified to download-only (aria2c)
- Removed dead code: _check_disk_space, UPLOAD_PROGRESS_INTERVAL,
  MIN_FREE_DISK_GB, UploadFile/File imports
- SCP Pull remains as separate /scp page
2026-07-23 12:33:23 +00:00
2d347692db fix: indentation error from aria2c edit 2026-07-23 11:21:07 +00:00
9110eb41bd feat: use aria2c for parallel downloads (8 connections, resumable)
Replaces wget with aria2c -x8 -s8 for significantly faster
downloads, especially on high-latency connections. Falls back to
single-connection if the server doesn't support range requests.
2026-07-23 11:19:13 +00:00
d050b8e6ed fix: progress bar min 1% + spinner for first 5% 2026-07-23 11:11:47 +00:00
344b3b189f debug: console.log upload progress events 2026-07-23 11:01:49 +00:00
5f2cb3dc64 fix: show MB during upload for values <100MB, GiB after
0.00008 GiB rounded to 0.0 — looked like no progress. Now shows
'16 MB' → '128 MB' → '0.5 GiB' → '1.9 GiB' as upload progresses.
2026-07-23 10:59:24 +00:00
39cf91cbc4 fix: corrupted event listeners and undefined sessionId from overzealous string replace
The Python replacement script changed ALL 'error' strings to
'pve-alert pve-alert-error', including:
- addEventListener('error', ...) → broken event listener
- data.phase === 'error' → never matched
- sessionId variable definition was lost/doubled

Restored proper event names and phase comparisons.
2026-07-23 10:56:50 +00:00
6db69f434c fix: Clean Up now deletes entire session directory tmp/{guid}/
- Backend cleanup_staging accepts session_id, deletes whole
  STAGING_ROOT/{guid}/ dir when provided
- CleanupRequest model gets session_id field
- Frontend cleanup proxy passes session_id from body
- api_client passes session_id to backend
- reuseImage JS includes session_id in cleanup payload
2026-07-23 10:54:42 +00:00
90bf8e4842 fix: remove unused out_dir from submit_job + Copy flow uses legacy fallback
1. submit_job() was creating empty tmp/out/{vmid}/ dirs via the old
   flat STAGING_OUT path. Removed — _process_job handles this via
   _staging_out() in the session-aware path.

2. Copy (re-convert) flow was hardcoding boot_type=uefi. Now uses
   auto_detect_boot=true with boot_type=legacy as fallback, matching
   the behavior of the original analyze→convert flow.
2026-07-23 10:52:20 +00:00
6617aca341 fix: purge all old staging path references — full code review cleanup
Bugs found and fixed:
- converter.py: STAGING_IN.iterdir() → extract_base.iterdir()
  (checked wrong dir when source was in session subdir)
- vm-bench.service: TMPDIR=/mnt/converter/in → /mnt/converter/tmp
  (old path poisoned all temp file writes)
- backend/app.py: removed unused STAGING_IN constant
- converter.py: removed unused STAGING_OUT constant
- install.sh: removed creation of old /in and /out dirs
2026-07-23 10:36:03 +00:00
1b145adea7 fix: extraction uses file's parent dir as base, not hardcoded STAGING_IN
extract_if_needed now extracts relative to the source file's parent
directory instead of /mnt/converter/tmp/in/. This correctly handles
files in tmp/{guid}/in/ paths.
2026-07-23 10:19:43 +00:00
7a024ca166 fix: correct staging path resolution — tmp/{guid}/in not tmp/in/{guid}
Frontend now passes '{guid}/in/filename' to backend.
Backend extract_if_needed uses STAGING_ROOT (tmp/) as base.
Provisioner staging helpers build tmp/{guid}/in + tmp/{guid}/out.
2026-07-23 10:14:19 +00:00
9dcde5d022 fix: flatten nested single-child directories after extraction
Archives like Debian_13_VMG.7z contain nested single-child dirs:
  Debian_13_VMG_LinuxVMImages.COM/Debian_13_VMG_LinuxVMImages.COM/

_flatten_nested() collapses these chains until files sit directly
in the parent, eliminating the double-nesting issue.
2026-07-23 10:09:25 +00:00
a28e2af674 refactor: move staging to /mnt/converter/tmp/{guid}/in + out 2026-07-23 10:07:57 +00:00
fc380f7d08 fix: add session_id to copyVM payload (Copy flow was missing it) 2026-07-23 09:51:30 +00:00
bc20691390 fix: prepend session GUID to filename when calling backend analyze
When files are stored in /mnt/converter/in/{guid}/, the backend
needs the full relative path. The analyze endpoint now constructs
'{guid}/{filename}' before passing to the backend API.
2026-07-23 09:46:28 +00:00
e53d05f7dc fix: add missing _staging_in helper + STAGING_IN constant 2026-07-23 09:36:50 +00:00
63659e4f93 feat: session GUID isolation for multi-user support
Each browser gets a UUID stored in localStorage, set as a cookie,
and attached to every API call. Files go to session-specific dirs:

  /mnt/converter/in/{guid}/    — uploads, downloads, SCP pulls
  /mnt/converter/out/{guid}/   — converted QCOW2s

Changes:
- base.html: generates UUID via crypto.randomUUID(), stores in
  localStorage + cookie, exposes as window.VM_BENCH_SID
- Frontend: all endpoints accept session_id, _staging() helper
  creates session-aware paths on demand
- JavaScript: session_id appended to all FormData, set as
  X-Session-ID header on raw uploads
- Backend models: JobSubmissionRequest.session_id field added
- Provisioner: _staging_in/_staging_out helpers, source path
  resolution uses session-aware directory
- Converter: extract_if_needed skips re-extraction if dir exists
2026-07-23 09:32:16 +00:00
7ded831354 fix: skip re-extraction if source directory already exists
When reusing a source image (Copy flow), the archive was already
extracted in /mnt/converter/in/. The extraction step failed with
'Destination path already exists' from 7z.

Now extract_if_needed checks if the expected output directory already
contains disk images, and returns it directly without re-extracting.
2026-07-23 09:28:02 +00:00
111d0d1331 chore: remove debug console.log from toggleSourceInput 2026-07-23 09:22:06 +00:00
fd4f8744d2 style: light Proxmox VE theme — white panels, blue header, light badges
Rewrote to match actual Proxmox VE light interface:
- #f0f0f0 page background, white panels
- Dark blue header (#1b3a5c) with white text
- Light gray panel headers (#f8f8f8)
- Bootstrap-style alert colors (yellow/blue/green/red)
- Orange accent throughout
2026-07-23 09:21:07 +00:00
272feefd36 debug: add console.log to toggleSourceInput 2026-07-23 09:19:36 +00:00
b2dbfe81f9 fix: custom select dropdown arrow visible on dark background
Native browser select arrows don't adapt to dark backgrounds.
Added appearance:none with a light SVG chevron via background-image.
2026-07-23 09:17:42 +00:00
4bebbd9da9 fix: add cache-busting ?v=2 to CSS link (nginx was caching old stylesheet) 2026-07-23 09:11:28 +00:00
844d86c105 style: Proxmox VE-inspired dark theme — pve-* class naming
Rewrote proxmox.css with consistent pve- prefix matching the Proxmox
VE design language: panels with orange-accent headers, clean tables,
orange progress bars, green/red/orange badges, subtle shadows.

Updated all templates (base, index, _analysis, polling, scp)
to use pve-btn, pve-input, pve-select, pve-panel, pve-progress,
pve-badge, pve-alert, pve-spinner, pve-hidden, pve-dim, etc.
2026-07-23 09:09:29 +00:00
492924f346 fix: remove dangling } from regex cleanup of submitReuse 2026-07-23 09:05:11 +00:00
9eb0f15261 feat: Clone (qm clone) + Copy (re-convert) buttons on completion
After conversion completes, the user sees a form with VM Name and ID
prefilled, and three buttons:

- Clone (instant): qm clone --full via backend, takes seconds
- Copy (re-convert): shows CPU/RAM/Disk fields, re-runs full pipeline
- Clean Up & Finish: deletes staging, returns to start page

Backend: new /api/v1/clone endpoint → provisioner.clone_vm()
Frontend: new /session/clone proxy, api_client.clone_vm()
2026-07-23 09:02:57 +00:00
7e1303a217 fix: auto-detect BIOS boot type + simplified reuse form
1. BIOS: provisioner now calls detect_efi() on the first disk when
   auto_detect_boot=true. Detects EFI partition via guestfish.
   Falls back to user's boot_type if detection inconclusive.

2. Reuse: 'Create Another VM' now shows an inline form asking only
   for VM Name and ID, reusing the same disk image, format, and
   boot settings from the completed job. No redirect needed.
2026-07-23 08:48:31 +00:00
80c5e2ceda fix: sanitize VM name + use qm disk import (newer Proxmox syntax)
- VM name now sanitized: spaces→hyphens, lowercase, strip invalid chars
- qm importdisk → qm disk import (newer Proxmox VE command)
2026-07-23 08:25:46 +00:00
334883dd26 fix: show download progress bar percentage when content_length is known
Previously always showed indeterminate spinner (pct=0) because
total size was unknown. Now if the HEAD request returns
Content-Length, the progress bar fills proportionally.
2026-07-23 08:12:21 +00:00
50e969f95e fix: restore download logic to session_upload — was embedded in upload_raw 2026-07-22 12:25:38 +00:00
adfb68fe6b fix: move HEAD request out of critical path — non-blocking background thread
The 10s HEAD request for Content-Length blocked the download endpoint
response. nginx proxy in front (bench.srv2.sechpoint.app:443 → :5000)
timed out waiting, returned empty response → 'data is null' JS error.

Now wget starts immediately, response returns instantly, and HEAD
runs in a daemon thread to populate content_length for ETA later.
2026-07-22 12:16:45 +00:00
c31ad3409a fix: restore missing progress bar width update in setPhase + cleanup debug logs
The fill.style.width = pct + '%' line was lost during a previous edit,
so the progress bar element was found but never visually updated.
Events fired, label updated, but bar stayed at 0%.
2026-07-22 11:49:08 +00:00
0f69e725a5 debug: add progress/loadstart console.logs 2026-07-22 09:16:12 +00:00
4428d91feb debug: add console.log to handleUpload entry point 2026-07-22 09:10:54 +00:00
f3477dc23b fix: raw streaming upload endpoint — bypasses multipart parser for large files
Starlette's multipart parser (pure Python) is too slow for >1 GB uploads —
boundary scanning over gigabytes blocks the event loop indefinitely.

New /session/upload-raw endpoint:
- Receives raw binary body via request.stream() — no parsing overhead
- Metadata (filename, vmid, vm_name) passed in HTTP headers
- Async chunked write directly to staging — true zero-copy streaming
- Same progress logging as before

Frontend now sends File object directly via xhr.send(file)
instead of FormData — eliminates multipart encoding on the client too.
2026-07-22 09:02:05 +00:00
837e674195 feat: SCP Pull page for large files from remote servers
New /scp page with form for host, user, password, remote path.
Runs scp via sshpass in background with progress polling.
Files go to /mnt/converter/in/{session_id}/ for multi-user isolation.

- Added sshpass + openssh-client to frontend installer
- scp.html template with full form + progress bar
- POST /scp/start — launches background SCP via sshpass -e
- GET /scp/progress/{sid}/{file} — polls file size + speed
- 'SCP Pull (+10 GB)' button on start page
2026-07-22 05:52:01 +00:00
1183059d8b fix: add no-cache headers + remove debug console.logs
- Cache-Control/Pragma/Expires headers prevent browser caching
  of JS-heavy pages (fixes stale code after updates)
- Removed debug console.log statements from upload handlers
2026-07-22 05:24:21 +00:00
6519fc1cf7 fix: use permanent DOM elements for progress instead of innerHTML reset
innerHTML reset in startSession was destroying #phase-label and
#progress-fill elements, then recreating them with same IDs. XHR
progress events fired asynchronously but getElementById might
return stale references or fail during the DOM update window.

Now #phase-label, #progress-fill, and #session-error are permanent
elements in the static HTML. startSession clears them via
textContent/style instead of innerHTML — no DOM destruction.
showError appends to #session-error instead of #session-status.
2026-07-22 05:19:13 +00:00