secubox-openwrt/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/updates.js
CyberMind-FR 2013ea2e8c fix: Add graceful fallback for RPC calls when backend not deployed
Fix 'No related RPC reply' errors by wrapping RPC calls in L.resolveDefault()
to provide fallback values when backend methods aren't available yet.

## Problem

When new LuCI views are deployed before backend packages, RPC calls fail with:
  Error: No related RPC reply

This happens because:
- Frontend (luci-app-secubox-admin) calls check_updates, get_catalog_sources
- Backend (secubox-core) hasn't been deployed yet with new RPCD methods
- RPCD returns no reply, causing frontend to crash

## Solution

Wrap all new RPC calls in L.resolveDefault() with sensible fallbacks:

**catalog-sources.js**:
- getCatalogSources() → fallback: { sources: [] }
- checkUpdates() → fallback: { updates: [] }

**updates.js**:
- checkUpdates() → fallback: { updates: [] }

This allows pages to load gracefully with empty data instead of crashing.

## Benefits

1. **Graceful degradation**: Pages load even without backend
2. **Deployment flexibility**: Can deploy frontend before backend
3. **Better UX**: Shows 'No updates' / 'No sources' instead of errors
4. **Production-ready**: Handles missing backends in production

## Testing

Before backend deployment:
- Catalog Sources page shows: 'No sources configured'
- Updates page shows: 'All applications are up to date'

After backend deployment:
- Pages populate with real data from RPCD

Incremented PKG_RELEASE: 4 → 5
2026-01-04 14:44:24 +01:00

353 lines
9.6 KiB
JavaScript

'use strict';
'require view';
'require secubox-admin.api as API';
'require secubox-admin.components as Components';
'require ui';
'require poll';
return view.extend({
load: function() {
return Promise.all([
L.resolveDefault(API.checkUpdates(), { updates: [] }),
API.getApps(),
API.getModules()
]);
},
render: function(data) {
var updateData = data[0] || {};
var apps = data[1].apps || [];
var modules = data[2].modules || {};
var self = this;
// Filter apps that have updates available
var updatesAvailable = updateData.updates || [];
var totalUpdates = updatesAvailable.length;
var container = E('div', { 'class': 'secubox-updates' }, [
E('link', { 'rel': 'stylesheet',
'href': L.resource('secubox-admin/common.css') }),
E('link', { 'rel': 'stylesheet',
'href': L.resource('secubox-admin/admin.css') }),
E('h2', {}, 'Available Updates'),
E('p', {}, 'Review and install available updates for SecuBox applications'),
// Summary header
E('div', { 'class': 'updates-summary' }, [
E('div', { 'class': 'summary-card' }, [
E('div', { 'class': 'summary-icon' }, '📦'),
E('div', { 'class': 'summary-info' }, [
E('div', { 'class': 'summary-count' }, totalUpdates.toString()),
E('div', { 'class': 'summary-label' }, totalUpdates === 1 ? 'Update Available' : 'Updates Available')
])
]),
E('div', { 'class': 'summary-actions' }, [
totalUpdates > 0 ? E('button', {
'class': 'btn btn-primary',
'click': function() {
self.updateAllApps(updatesAvailable);
}
}, 'Update All') : null,
E('button', {
'class': 'btn btn-secondary',
'click': function() {
self.checkForUpdates();
}
}, 'Check for Updates')
])
]),
// Updates list
totalUpdates > 0 ?
E('div', { 'class': 'updates-list', 'id': 'updates-list' },
updatesAvailable.map(function(update) {
// Find full app details from catalog
var app = apps.find(function(a) { return a.id === update.app_id; });
return self.renderUpdateCard(update, app);
})
) :
E('div', { 'class': 'no-updates' }, [
E('div', { 'class': 'no-updates-icon' }, '✓'),
E('h3', {}, 'All applications are up to date'),
E('p', {}, 'Check back later for new updates or click "Check for Updates" to refresh.')
])
]);
// Auto-refresh every 60 seconds
poll.add(function() {
return API.checkUpdates().then(function(result) {
var updatesList = document.getElementById('updates-list');
if (updatesList && result.updates) {
// Only update if count changed
if (result.updates.length !== totalUpdates) {
window.location.reload();
}
}
});
}, 60);
return container;
},
renderUpdateCard: function(update, app) {
var self = this;
if (!app) {
// App not found in catalog, show minimal info
app = {
id: update.app_id,
name: update.app_id,
description: 'Application from catalog',
icon: '📦'
};
}
return E('div', { 'class': 'update-card', 'data-app-id': update.app_id }, [
// App header
E('div', { 'class': 'update-header' }, [
E('div', { 'class': 'app-icon-large' }, app.icon || '📦'),
E('div', { 'class': 'app-title' }, [
E('h3', {}, app.name),
E('p', { 'class': 'app-category' }, app.category || 'Application')
]),
E('div', { 'class': 'update-badge' }, [
E('span', { 'class': 'badge badge-warning' }, 'UPDATE AVAILABLE')
])
]),
// Version info
E('div', { 'class': 'version-info' }, [
E('div', { 'class': 'version-row' }, [
E('span', { 'class': 'version-label' }, 'Current Version:'),
E('span', { 'class': 'version-value current' }, update.installed_version || 'Unknown')
]),
E('div', { 'class': 'version-arrow' }, '→'),
E('div', { 'class': 'version-row' }, [
E('span', { 'class': 'version-label' }, 'New Version:'),
E('span', { 'class': 'version-value new' }, update.catalog_version || 'Unknown')
])
]),
// Changelog section
update.changelog ? E('div', { 'class': 'changelog-section' }, [
E('h4', {}, 'What\'s New'),
E('div', { 'class': 'changelog-content' },
this.renderChangelog(update.changelog)
)
]) : null,
// Update info
E('div', { 'class': 'update-meta' }, [
update.release_date ? E('div', { 'class': 'meta-item' }, [
E('span', { 'class': 'meta-label' }, 'Release Date:'),
E('span', { 'class': 'meta-value' }, update.release_date)
]) : null,
update.download_size ? E('div', { 'class': 'meta-item' }, [
E('span', { 'class': 'meta-label' }, 'Download Size:'),
E('span', { 'class': 'meta-value' }, API.formatBytes(update.download_size))
]) : null
]),
// Actions
E('div', { 'class': 'update-actions' }, [
E('button', {
'class': 'btn btn-primary',
'click': function() {
self.updateApp(update.app_id, app.name);
}
}, 'Update Now'),
E('button', {
'class': 'btn btn-secondary',
'click': function() {
self.viewFullChangelog(update.app_id, update.installed_version, update.catalog_version);
}
}, 'View Full Changelog'),
E('button', {
'class': 'btn btn-tertiary',
'click': function() {
self.skipUpdate(update.app_id);
}
}, 'Skip This Version')
])
]);
},
renderChangelog: function(changelog) {
if (typeof changelog === 'string') {
return E('p', {}, changelog);
}
if (Array.isArray(changelog)) {
return E('ul', { 'class': 'changelog-list' },
changelog.map(function(item) {
return E('li', {}, item);
})
);
}
// Object with version keys
var versions = Object.keys(changelog);
if (versions.length > 0) {
var latestVersion = versions[0];
var changes = changelog[latestVersion].changes || [];
return E('ul', { 'class': 'changelog-list' },
changes.map(function(item) {
return E('li', {}, item);
})
);
}
return E('p', {}, 'No changelog available');
},
updateApp: function(appId, appName) {
ui.showModal(_('Updating Application'), [
E('p', { 'class': 'spinning' }, _('Updating %s...').format(appName))
]);
API.installApp(appId).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null,
E('p', _('%s updated successfully').format(appName)),
'success'
);
// Refresh the page to show updated status
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
ui.addNotification(null,
E('p', _('Update failed: %s').format(result.error || 'Unknown error')),
'error'
);
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null,
E('p', _('Update error: %s').format(err.message)),
'error'
);
});
},
updateAllApps: function(updates) {
if (updates.length === 0) return;
ui.showModal(_('Updating Applications'), [
E('p', { 'class': 'spinning' }, _('Updating %d applications...').format(updates.length)),
E('p', { 'id': 'update-progress' }, _('Preparing...'))
]);
var self = this;
var currentIndex = 0;
function updateNext() {
if (currentIndex >= updates.length) {
ui.hideModal();
ui.addNotification(null,
E('p', _('All applications updated successfully')),
'success'
);
setTimeout(function() {
window.location.reload();
}, 1000);
return;
}
var update = updates[currentIndex];
var progressEl = document.getElementById('update-progress');
if (progressEl) {
progressEl.textContent = _('Updating %s (%d/%d)...')
.format(update.app_id, currentIndex + 1, updates.length);
}
API.installApp(update.app_id).then(function(result) {
currentIndex++;
updateNext();
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null,
E('p', _('Failed to update %s: %s').format(update.app_id, err.message)),
'error'
);
});
}
updateNext();
},
viewFullChangelog: function(appId, fromVersion, toVersion) {
API.getChangelog(appId, fromVersion, toVersion).then(function(changelog) {
var content = E('div', { 'class': 'changelog-modal' }, [
E('h3', {}, _('Changelog for %s').format(appId)),
E('div', { 'class': 'version-range' },
_('Changes from %s to %s').format(fromVersion, toVersion)
),
E('div', { 'class': 'changelog-full' },
// Render full changelog
typeof changelog === 'string' ?
E('pre', {}, changelog) :
JSON.stringify(changelog, null, 2)
)
]);
ui.showModal(_('Full Changelog'), [
content,
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': function() {
ui.hideModal();
}
}, _('Close'))
])
]);
}).catch(function(err) {
ui.addNotification(null,
E('p', _('Failed to load changelog: %s').format(err.message)),
'error'
);
});
},
skipUpdate: function(appId) {
// TODO: Implement skip version functionality
// This would mark the version as skipped in metadata
ui.addNotification(null,
E('p', _('Skipped update for: %s').format(appId)),
'info'
);
},
checkForUpdates: function() {
ui.showModal(_('Checking for Updates'), [
E('p', { 'class': 'spinning' }, _('Checking for available updates...'))
]);
// Sync catalog first, then check for updates
API.syncCatalog(null).then(function() {
return API.checkUpdates();
}).then(function(result) {
ui.hideModal();
ui.addNotification(null,
E('p', _('Update check complete. Found %d updates.')
.format((result.updates || []).length)),
'success'
);
window.location.reload();
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null,
E('p', _('Update check failed: %s').format(err.message)),
'error'
);
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});