diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f7a59343..171aa22b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -326,7 +326,8 @@ "Bash(fuser:*)", "Bash(lsof:*)", "Bash(arp:*)", - "Bash(ip link:*)" + "Bash(ip link:*)", + "Bash(git describe:*)" ] } } diff --git a/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js index 2da36d81..22ebc4b4 100644 --- a/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js +++ b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js @@ -37,7 +37,14 @@ var callGenerateToken = rpc.declare({ var callBuildImage = rpc.declare({ object: 'luci.cloner', - method: 'build_image' + method: 'build_image', + params: ['device_type'] +}); + +var callListDevices = rpc.declare({ + object: 'luci.cloner', + method: 'list_devices', + expect: { devices: [] } }); var callTftpStart = rpc.declare({ @@ -62,7 +69,8 @@ return view.extend({ callGetStatus(), callListImages(), callListTokens(), - callListClones() + callListClones(), + callListDevices() ]); }, @@ -71,6 +79,7 @@ return view.extend({ var images = data[1].images || []; var tokens = data[2].tokens || []; var clones = data[3].clones || []; + var devices = data[4].devices || []; var view = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, 'Cloning Station'), @@ -117,13 +126,14 @@ return view.extend({ E('h3', {}, 'Clone Images'), E('table', { 'class': 'table', 'id': 'images-table' }, [ E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Device'), E('th', { 'class': 'th' }, 'Name'), E('th', { 'class': 'th' }, 'Size'), E('th', { 'class': 'th' }, 'TFTP Ready'), E('th', { 'class': 'th' }, 'Actions') ]) ].concat(images.length > 0 ? images.map(L.bind(this.renderImageRow, this)) : - [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' }, + [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 5, 'style': 'text-align:center;' }, 'No images available. Click "Build Image" to create one.')])] )) ]), @@ -194,10 +204,17 @@ return view.extend({ }, renderImageRow: function(img) { + var deviceBadge = E('span', { + 'style': 'padding:2px 8px;border-radius:4px;font-size:12px;background:#3b82f622;color:#3b82f6;' + }, img.device || 'unknown'); + return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, img.name), + E('td', { 'class': 'td' }, deviceBadge), + E('td', { 'class': 'td', 'style': 'font-family:monospace;font-size:12px;' }, img.name), E('td', { 'class': 'td' }, img.size), - E('td', { 'class': 'td' }, img.tftp_ready ? 'Yes' : 'No'), + E('td', { 'class': 'td' }, img.tftp_ready ? + E('span', { 'style': 'color:#22c55e;' }, 'Ready') : + E('span', { 'style': 'color:#f59e0b;' }, 'Pending')), E('td', { 'class': 'td' }, '-') ]); }, @@ -231,20 +248,34 @@ return view.extend({ }, handleBuild: function() { - ui.showModal('Build Clone Image', [ - E('p', {}, 'This will build a clone image of the current system.'), - E('p', {}, 'The image can then be flashed to other devices of the same type.'), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), - ' ', - E('button', { 'class': 'cbi-button cbi-button-positive', 'click': L.bind(function() { - ui.hideModal(); - callBuildImage().then(L.bind(function(res) { - ui.addNotification(null, E('p', res.message || 'Build started'), 'info'); - }, this)); - }, this) }, 'Build') - ]) - ]); + var self = this; + callListDevices().then(function(data) { + var devices = data.devices || []; + var select = E('select', { 'id': 'device-select', 'class': 'cbi-input-select', 'style': 'width:100%;' }); + + devices.forEach(function(dev) { + select.appendChild(E('option', { 'value': dev.id }, dev.name + ' (' + dev.cpu + ')')); + }); + + ui.showModal('Build Clone Image', [ + E('p', {}, 'Select the target device type to build an image for:'), + E('div', { 'style': 'margin:15px 0;' }, select), + E('p', { 'style': 'color:#888;font-size:12px;' }, 'The image will be built via ASU API and may take several minutes.'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + ' ', + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + var deviceType = document.getElementById('device-select').value; + ui.hideModal(); + ui.addNotification(null, E('p', 'Building image for ' + deviceType + '...'), 'info'); + callBuildImage(deviceType).then(function(res) { + ui.addNotification(null, E('p', res.message || 'Build started'), 'info'); + self.refresh(); + }); + } }, 'Build') + ]) + ]); + }); }, handleTftp: function(start) { diff --git a/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner b/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner index 939a9f5b..11562500 100755 --- a/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner +++ b/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner @@ -86,30 +86,41 @@ do_status() { } do_list_images() { - local img name + local img name device_type json_init json_add_array "images" - # TFTP ready image - if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then + # All TFTP-ready images + for img in "$TFTP_ROOT"/secubox-clone*.img; do + [ -f "$img" ] || continue + name=$(basename "$img") + # Extract device type from filename + device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img/\1/p') + [ -z "$device_type" ] && device_type="mochabin" json_add_object "" - json_add_string "name" "secubox-clone.img" - json_add_string "path" "$TFTP_ROOT/secubox-clone.img" - json_add_string "size" "$(ls -lh "$TFTP_ROOT/secubox-clone.img" | awk '{print $5}')" + json_add_string "name" "$name" + json_add_string "path" "$img" + json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')" + json_add_string "device" "$device_type" json_add_boolean "tftp_ready" 1 json_close_object - fi + done - # Clone directory images + # Clone directory images (not yet in TFTP) if [ -d "$CLONE_DIR" ]; then for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz; do [ -f "$img" ] || continue name=$(basename "$img") + # Skip if already in TFTP + [ -f "$TFTP_ROOT/${name%.gz}" ] && continue + device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img.*/\1/p') + [ -z "$device_type" ] && device_type="unknown" json_add_object "" json_add_string "name" "$name" json_add_string "path" "$img" json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')" + json_add_string "device" "$device_type" json_add_boolean "tftp_ready" 0 json_close_object done @@ -219,12 +230,23 @@ EOF } do_build_image() { + local input device_type + + read input + device_type=$(echo "$input" | jsonfilter -e '@.device_type' 2>/dev/null) + json_init if [ -x /usr/sbin/secubox-cloner ]; then - (/usr/sbin/secubox-cloner build 2>&1 > /tmp/cloner-build.log) & - json_add_boolean "success" 1 - json_add_string "message" "Build started in background" + if [ -n "$device_type" ]; then + (/usr/sbin/secubox-cloner build "$device_type" 2>&1 > /tmp/cloner-build.log) & + json_add_boolean "success" 1 + json_add_string "message" "Build started for $device_type" + else + (/usr/sbin/secubox-cloner build 2>&1 > /tmp/cloner-build.log) & + json_add_boolean "success" 1 + json_add_string "message" "Build started for current device" + fi else json_add_boolean "success" 0 json_add_string "message" "secubox-cloner not installed" @@ -233,6 +255,38 @@ do_build_image() { json_dump } +do_list_devices() { + json_init + json_add_array "devices" + + json_add_object "" + json_add_string "id" "mochabin" + json_add_string "name" "Globalscale MOCHAbin" + json_add_string "cpu" "Cortex-A72" + json_close_object + + json_add_object "" + json_add_string "id" "espressobin-v7" + json_add_string "name" "Globalscale ESPRESSObin v7" + json_add_string "cpu" "Cortex-A53" + json_close_object + + json_add_object "" + json_add_string "id" "espressobin-ultra" + json_add_string "name" "Globalscale ESPRESSObin Ultra" + json_add_string "cpu" "Cortex-A53" + json_close_object + + json_add_object "" + json_add_string "id" "x86-64" + json_add_string "name" "Generic x86-64" + json_add_string "cpu" "x86_64" + json_close_object + + json_close_array + json_dump +} + do_tftp_start() { json_init @@ -301,8 +355,9 @@ case "$1" in echo '"list_images":{},' echo '"list_tokens":{},' echo '"list_clones":{},' + echo '"list_devices":{},' echo '"generate_token":{"auto_approve":"Boolean"},' - echo '"build_image":{},' + echo '"build_image":{"device_type":"String"},' echo '"tftp_start":{},' echo '"tftp_stop":{},' echo '"delete_token":{"token":"String"},' @@ -315,6 +370,7 @@ case "$1" in list_images) do_list_images ;; list_tokens) do_list_tokens ;; list_clones) do_list_clones ;; + list_devices) do_list_devices ;; generate_token) do_generate_token ;; build_image) do_build_image ;; tftp_start) do_tftp_start ;; diff --git a/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json b/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json index adcb9429..b52f5ef8 100644 --- a/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json +++ b/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json @@ -3,7 +3,7 @@ "description": "Grant access to SecuBox Cloning Station", "read": { "ubus": { - "luci.cloner": ["status", "list_images", "list_tokens", "list_clones"] + "luci.cloner": ["status", "list_images", "list_tokens", "list_clones", "list_devices"] } }, "write": { diff --git a/package/secubox/secubox-core/root/usr/sbin/secubox-cloner b/package/secubox/secubox-core/root/usr/sbin/secubox-cloner index 0773f52b..76f40562 100755 --- a/package/secubox/secubox-core/root/usr/sbin/secubox-cloner +++ b/package/secubox/secubox-core/root/usr/sbin/secubox-cloner @@ -6,8 +6,9 @@ # Cloned devices auto-resize root and join mesh as peers. # # Usage: -# secubox-cloner build [--resize SIZE] Build clone image for current device +# secubox-cloner build [--device TYPE] [--resize SIZE] Build clone image # secubox-cloner serve [--start|--stop] Manage TFTP clone server +# secubox-cloner devices List supported device types # secubox-cloner token [--auto-approve] Generate clone join token # secubox-cloner status Show cloner status # secubox-cloner list List pending/joined clones @@ -96,31 +97,41 @@ get_device_id() { # ============================================================================ build_image() { - local resize_target="${1:-}" + local device_type="${1:-}" + local resize_target="${2:-}" log "Building clone image..." - # Detect device type - local device_type=$(detect_device) - if [ "$device_type" = "unknown" ]; then - error "Could not detect device type" - return 1 + # Use specified device type or detect current + if [ -z "$device_type" ]; then + device_type=$(detect_device) fi + # Validate device type + case "$device_type" in + mochabin|espressobin-v7|espressobin-ultra|x86-64) + ;; + *) + error "Unknown device type: $device_type" + error "Supported: mochabin, espressobin-v7, espressobin-ultra, x86-64" + return 1 + ;; + esac + log "Device type: $device_type" # Create directories mkdir -p "$CLONE_DIR" mkdir -p "$TFTP_ROOT" - # Method 1: Export current firmware (if sysupgrade backup works) - # This creates a perfect clone of the running system + # Image paths - each device type has its own image local image_name="secubox-clone-${device_type}.img" local image_path="$CLONE_DIR/$image_name" + local tftp_image="$TFTP_ROOT/${image_name}" - # Check if we have a pre-built image we can use - local existing_image=$(ls -t "$CLONE_DIR"/*.img.gz 2>/dev/null | head -1) - if [ -n "$existing_image" ]; then + # Check if we have a pre-built image for this device type + local existing_image=$(ls -t "$CLONE_DIR"/secubox-clone-${device_type}.img* 2>/dev/null | head -1) + if [ -n "$existing_image" ] && [ -f "$existing_image" ]; then log "Using existing image: $existing_image" image_path="$existing_image" else @@ -144,10 +155,9 @@ build_image() { resize_image "$image_path" "$resize_target" fi - # Copy to TFTP root + # Copy to TFTP root with device-specific name log "Copying to TFTP root..." if [ -f "$image_path" ]; then - local tftp_image="$TFTP_ROOT/secubox-clone.img" if echo "$image_path" | grep -q "\.gz$"; then gunzip -c "$image_path" > "$tftp_image" else @@ -155,18 +165,36 @@ build_image() { fi log "Clone image ready: $tftp_image" log "Size: $(ls -lh "$tftp_image" | awk '{print $5}')" + + # Also create generic symlink for backward compatibility + ln -sf "$image_name" "$TFTP_ROOT/secubox-clone.img" 2>/dev/null fi # Save state cat > "$STATE_FILE" <