This commit includes comprehensive codebase cleanup and refactoring: ## Code Cleanup - Remove dead TypeScript code (types/monitoring.ts - 194 lines duplicate) - Remove unused Go functions (GetClusterNodes, MigratePassword, GetClusterHealthInfo) - Clean up commented-out code blocks across multiple files - Remove unused TypeScript exports (helpTextClass, private tag color helpers) - Delete obsolete test files and components ## localStorage Consolidation - Centralize all storage keys into STORAGE_KEYS constant - Update 5 files to use centralized keys: * utils/apiClient.ts (AUTH, LEGACY_TOKEN) * components/Dashboard/Dashboard.tsx (GUEST_METADATA) * components/Docker/DockerHosts.tsx (DOCKER_METADATA) * App.tsx (PLATFORMS_SEEN) * stores/updates.ts (UPDATES) - Benefits: Single source of truth, prevents typos, better maintainability ## Previous Work Committed - Docker monitoring improvements and disk metrics - Security enhancements and setup fixes - API refactoring and cleanup - Documentation updates - Build system improvements ## Testing - All frontend tests pass (29 tests) - All Go tests pass (15 packages) - Production build successful - Zero breaking changes Total: 186 files changed, 5825 insertions(+), 11602 deletions(-)
219 lines
5.9 KiB
TypeScript
219 lines
5.9 KiB
TypeScript
import { createSignal } from 'solid-js';
|
|
import { UpdatesAPI } from '@/api/updates';
|
|
import type { UpdateInfo, VersionInfo } from '@/api/updates';
|
|
import { getPulseHostname } from '@/utils/url';
|
|
import { logger } from '@/utils/logger';
|
|
import { STORAGE_KEYS } from '@/utils/localStorage';
|
|
|
|
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
interface UpdateState {
|
|
lastCheck: number;
|
|
dismissedVersion?: string;
|
|
updateInfo?: UpdateInfo;
|
|
}
|
|
|
|
// Load state from localStorage
|
|
const loadState = (): UpdateState => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEYS.UPDATES);
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
logger.error('Failed to load update state:', e);
|
|
}
|
|
return { lastCheck: 0 };
|
|
};
|
|
|
|
// Save state to localStorage
|
|
const saveState = (state: UpdateState) => {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEYS.UPDATES, JSON.stringify(state));
|
|
} catch (e) {
|
|
logger.error('Failed to save update state:', e);
|
|
}
|
|
};
|
|
|
|
// Create signals
|
|
const [updateAvailable, setUpdateAvailable] = createSignal(false);
|
|
const [updateInfo, setUpdateInfo] = createSignal<UpdateInfo | null>(null);
|
|
const [versionInfo, setVersionInfo] = createSignal<VersionInfo | null>(null);
|
|
const [isChecking, setIsChecking] = createSignal(false);
|
|
const [isDismissed, setIsDismissed] = createSignal(false);
|
|
const [lastError, setLastError] = createSignal<string | null>(null);
|
|
|
|
// Check for updates
|
|
const checkForUpdates = async (force = false): Promise<void> => {
|
|
// Don't check if already checking
|
|
if (isChecking()) return;
|
|
|
|
const state = loadState();
|
|
const now = Date.now();
|
|
|
|
// Skip if checked recently (unless forced)
|
|
if (!force && state.lastCheck && now - state.lastCheck < CHECK_INTERVAL) {
|
|
// Use cached data if available
|
|
if (state.updateInfo) {
|
|
// First check if version matches (in case user updated)
|
|
try {
|
|
const currentVersion = await UpdatesAPI.getVersion();
|
|
if (state.updateInfo.currentVersion !== currentVersion.version) {
|
|
// Version changed, invalidate cache and check again
|
|
state.updateInfo = undefined;
|
|
state.dismissedVersion = undefined;
|
|
state.lastCheck = 0;
|
|
saveState(state);
|
|
// Continue to check for updates
|
|
} else {
|
|
// Version matches, use cached data
|
|
setUpdateInfo(state.updateInfo);
|
|
setUpdateAvailable(state.updateInfo.available);
|
|
|
|
// Check if this version was dismissed
|
|
if (state.dismissedVersion === state.updateInfo.latestVersion) {
|
|
setIsDismissed(true);
|
|
}
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// If we can't get version, continue with normal check
|
|
logger.error('Failed to verify version for cache:', e);
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
setIsChecking(true);
|
|
setLastError(null);
|
|
|
|
try {
|
|
// First get version info to check deployment type
|
|
const version = await UpdatesAPI.getVersion();
|
|
setVersionInfo(version);
|
|
|
|
// Clear cache if version has changed (user updated)
|
|
if (state.updateInfo && state.updateInfo.currentVersion !== version.version) {
|
|
// Version changed, clear the cache
|
|
state.updateInfo = undefined;
|
|
state.dismissedVersion = undefined;
|
|
saveState(state);
|
|
}
|
|
|
|
// Don't check for updates in Docker or development builds
|
|
if (version.isDocker || version.isDevelopment || version.isSourceBuild) {
|
|
setUpdateAvailable(false);
|
|
setUpdateInfo(null);
|
|
return;
|
|
}
|
|
|
|
// Skip dev builds unless forcing (contains -dirty or commit hash after version)
|
|
const isDirtyBuild =
|
|
version.version.includes('-dirty') || /v\d+\.\d+\.\d+.*-g[0-9a-f]+/.test(version.version);
|
|
if (isDirtyBuild && !force) {
|
|
setUpdateAvailable(false);
|
|
return;
|
|
}
|
|
|
|
// Get the saved update channel from system settings
|
|
const info = await UpdatesAPI.checkForUpdates();
|
|
|
|
setUpdateInfo(info);
|
|
setUpdateAvailable(info.available);
|
|
|
|
// Check if this version was dismissed
|
|
if (state.dismissedVersion === info.latestVersion) {
|
|
setIsDismissed(true);
|
|
} else {
|
|
setIsDismissed(false);
|
|
}
|
|
|
|
// Save to cache
|
|
saveState({
|
|
...state,
|
|
lastCheck: now,
|
|
updateInfo: info,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to check for updates:', error);
|
|
setLastError(error instanceof Error ? error.message : 'Failed to check for updates');
|
|
setUpdateAvailable(false);
|
|
} finally {
|
|
setIsChecking(false);
|
|
}
|
|
};
|
|
|
|
// Dismiss current update
|
|
const dismissUpdate = () => {
|
|
const info = updateInfo();
|
|
if (!info) return;
|
|
|
|
const state = loadState();
|
|
saveState({
|
|
...state,
|
|
dismissedVersion: info.latestVersion,
|
|
});
|
|
|
|
setIsDismissed(true);
|
|
};
|
|
|
|
// Clear dismissed version (useful when user wants to see the update again)
|
|
const clearDismissed = () => {
|
|
const state = loadState();
|
|
delete state.dismissedVersion;
|
|
saveState(state);
|
|
setIsDismissed(false);
|
|
};
|
|
|
|
// Check if update is visible (available and not dismissed)
|
|
const isUpdateVisible = () => updateAvailable() && !isDismissed();
|
|
|
|
// Export store
|
|
export const updateStore = {
|
|
// State
|
|
updateAvailable,
|
|
updateInfo,
|
|
versionInfo,
|
|
isChecking,
|
|
isDismissed,
|
|
lastError,
|
|
isUpdateVisible,
|
|
|
|
// Actions
|
|
checkForUpdates,
|
|
dismissUpdate,
|
|
clearDismissed,
|
|
|
|
// Manual testing helpers
|
|
simulateUpdate: (version: string = 'v5.0.0') => {
|
|
setUpdateInfo({
|
|
available: true,
|
|
currentVersion: versionInfo()?.version || 'v4.9.0',
|
|
latestVersion: version,
|
|
releaseNotes: 'Test update notification',
|
|
releaseDate: new Date().toISOString(),
|
|
downloadUrl: '#',
|
|
isPrerelease: false,
|
|
});
|
|
setUpdateAvailable(true);
|
|
setIsDismissed(false);
|
|
},
|
|
};
|
|
|
|
// Expose for testing in development
|
|
declare global {
|
|
interface Window {
|
|
updateStore?: typeof updateStore;
|
|
}
|
|
}
|
|
|
|
const pulseHostname = getPulseHostname();
|
|
|
|
if (
|
|
import.meta.env.DEV ||
|
|
pulseHostname === 'localhost' ||
|
|
pulseHostname.startsWith('192.168')
|
|
) {
|
|
window.updateStore = updateStore;
|
|
}
|