From 78d4e1bb54b4187d82f6eea2a9b1371b4783b6cb Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:55:26 +0100 Subject: [PATCH] ui fix --- app/components/settings/debug/DebugTab.tsx | 195 +++-- .../settings/developer/DeveloperWindow.tsx | 240 ++++-- .../settings/features/FeaturesTab.tsx | 386 +++++----- .../notifications/NotificationsTab.tsx | 102 +-- app/components/settings/settings.types.ts | 1 + app/components/settings/update/UpdateTab.tsx | 718 +++++++++++++++--- app/components/settings/user/UsersWindow.tsx | 225 ++++-- app/lib/api/notifications.ts | 108 ++- app/lib/hooks/useNotifications.ts | 44 +- app/lib/stores/logs.ts | 52 ++ package.json | 1 + pnpm-lock.yaml | 435 +++++++++++ scripts/update.sh | 52 ++ 13 files changed, 2011 insertions(+), 548 deletions(-) create mode 100755 scripts/update.sh diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index 330e2cb4..2d4879eb 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { classNames } from '~/utils/classNames'; import { logStore } from '~/lib/stores/logs'; +import type { LogEntry } from '~/lib/stores/logs'; interface ProviderStatus { id: string; @@ -472,51 +473,147 @@ export default function DebugTab() { } }; - const handleCheckErrors = () => { + const checkErrors = async () => { try { setLoading((prev) => ({ ...prev, errors: true })); - // Get any errors from the performance entries - const resourceErrors = performance - .getEntriesByType('resource') - .filter((entry) => { - const failedEntry = entry as PerformanceResourceTiming; - return failedEntry.responseEnd - failedEntry.startTime === 0; - }) - .map((entry) => ({ - type: 'networkError', - resource: entry.name, - timestamp: new Date().toISOString(), - })); + // Get errors from log store + const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error'); - // Combine collected errors with resource errors - const allErrors = [...errorLog.errors, ...resourceErrors]; + // Combine with runtime errors + const allErrors = [ + ...errorLog.errors, + ...storedErrors.map((error) => ({ + type: 'stored', + message: error.message, + timestamp: error.timestamp, + details: error.details || {}, + })), + ]; - if (allErrors.length > 0) { - logStore.logError('JavaScript Errors Found', { - errors: allErrors, - timestamp: new Date().toISOString(), - }); - toast.error(`Found ${allErrors.length} error(s)`); - } else { - toast.success('No errors found'); - } - - // Update error log setErrorLog({ errors: allErrors, lastCheck: new Date().toISOString(), }); + + if (allErrors.length === 0) { + toast.success('No errors found'); + } else { + toast.warning(`Found ${allErrors.length} error(s)`); + } } catch (error) { - toast.error('Failed to check for errors'); - console.error('Failed to check for errors:', error); + toast.error('Failed to check errors'); + console.error('Failed to check errors:', error); } finally { setLoading((prev) => ({ ...prev, errors: false })); } }; return ( -
+
+ {/* Action Buttons */} +
+ + + + + + + +
+ + {/* Error Log Display */} + {errorLog.errors.length > 0 && ( +
+

Error Log

+
+ {errorLog.errors.map((error, index) => ( +
+
+ Type: {error.type} + Time: + {new Date(error.timestamp).toLocaleString()} +
+
{error.message}
+ {error.filename && ( +
+ File: {error.filename} (Line: {error.lineNumber}, Column: {error.columnNumber}) +
+ )} +
+ ))} +
+
+ )} + {/* System Information */}
@@ -529,9 +626,9 @@ export default function DebugTab() { onClick={handleLogSystemInfo} className={classNames( 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm', - 'bg-[#F5F5F5] dark:bg-[#1A1A1A]', - 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]', - 'transition-colors', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]', + 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary', + 'transition-colors duration-200', )} >
@@ -541,10 +638,12 @@ export default function DebugTab() { onClick={getSystemInfo} className={classNames( 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm', - 'bg-[#F5F5F5] dark:bg-[#1A1A1A]', - 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]', - 'transition-colors', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]', + 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary', + 'transition-colors duration-200', + { 'opacity-50 cursor-not-allowed': loading.systemInfo }, )} + disabled={loading.systemInfo} >
Refresh @@ -684,10 +783,12 @@ export default function DebugTab() { onClick={checkProviderStatus} className={classNames( 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm', - 'bg-[#F5F5F5] dark:bg-[#1A1A1A]', - 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]', - 'transition-colors', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]', + 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary', + 'transition-colors duration-200', + { 'opacity-50 cursor-not-allowed': loading.providers }, )} + disabled={loading.providers} >
Refresh @@ -729,10 +830,12 @@ export default function DebugTab() { onClick={handleLogPerformance} className={classNames( 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm', - 'bg-[#F5F5F5] dark:bg-[#1A1A1A]', - 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]', - 'transition-colors', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]', + 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary', + 'transition-colors duration-200', + { 'opacity-50 cursor-not-allowed': loading.performance }, )} + disabled={loading.performance} >
Log Performance @@ -811,13 +914,15 @@ export default function DebugTab() {

Error Check

+ ) : ( + + )} + + {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'} + +
+ +
+ {!activeTab && !showTabManagement && ( + setShowTabManagement(true)} + className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > -
+
+ + Manage Tabs + )} -
- -

- {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'} -

+ +
+ + + + + + + + handleTabClick('profile')} + > +
+
+
+ Profile + + + handleTabClick('settings')} + > +
+
+
+ Settings + + + {profile.notifications && ( + <> + handleTabClick('notifications')} + > +
+
+
+ + Notifications + {hasUnreadNotifications && ( + + {unreadNotifications.length} + + )} + + + + + + )} + +
+
+
+ Close + + + +
-
-
- {!showTabManagement && !activeTab && ( - setShowTabManagement(true)} - className={classNames( - 'px-3 py-1.5 rounded-lg text-sm', - 'bg-purple-500/10 text-purple-500', - 'hover:bg-purple-500/20', - 'transition-colors duration-200', - )} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - Manage Tabs - - )} - -
- +
+
diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index aec785a4..c87652e7 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo } from 'react'; import { motion } from 'framer-motion'; import { Switch } from '~/components/ui/Switch'; import { useSettings } from '~/lib/hooks/useSettings'; @@ -19,6 +19,93 @@ interface FeatureToggle { tooltip?: string; } +const FeatureCard = memo( + ({ + feature, + index, + onToggle, + }: { + feature: FeatureToggle; + index: number; + onToggle: (id: string, enabled: boolean) => void; + }) => ( + +
+
+
+
+
+

{feature.title}

+ {feature.beta && ( + Beta + )} + {feature.experimental && ( + + Experimental + + )} +
+
+ onToggle(feature.id, checked)} /> +
+

{feature.description}

+ {feature.tooltip &&

{feature.tooltip}

} +
+ + ), +); + +const FeatureSection = memo( + ({ + title, + features, + icon, + description, + onToggleFeature, + }: { + title: string; + features: FeatureToggle[]; + icon: string; + description: string; + onToggleFeature: (id: string, enabled: boolean) => void; + }) => ( + +
+
+
+

{title}

+

{description}

+
+
+ +
+ {features.map((feature, index) => ( + + ))} +
+ + ), +); + export default function FeaturesTab() { const { setEventLogs, @@ -36,50 +123,56 @@ export default function FeaturesTab() { const eventLogs = useStore(isEventLogsEnabled); - const features: FeatureToggle[] = [ - { - id: 'latestBranch', - title: 'Use Main Branch', - description: 'Check for updates against the main branch instead of stable', - icon: 'i-ph:git-branch', - enabled: isLatestBranch, - beta: true, - tooltip: 'Get the latest features and improvements before they are officially released', - }, - { - id: 'autoTemplate', - title: 'Auto Select Code Template', - description: 'Let Bolt select the best starter template for your project', - icon: 'i-ph:magic-wand', - enabled: autoSelectTemplate, - tooltip: 'Automatically choose the most suitable template based on your project type', - }, - { - id: 'contextOptimization', - title: 'Context Optimization', - description: 'Optimize chat context by redacting file contents and using system prompts', - icon: 'i-ph:arrows-in', - enabled: contextOptimizationEnabled, - tooltip: 'Improve AI responses by optimizing the context window and system prompts', - }, - { - id: 'experimentalProviders', - title: 'Experimental Providers', - description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike', - icon: 'i-ph:robot', - enabled: isLocalModel, - experimental: true, - tooltip: 'Try out new AI providers and models in development', - }, - { - id: 'eventLogs', - title: 'Event Logging', - description: 'Enable detailed event logging and history', - icon: 'i-ph:list-bullets', - enabled: eventLogs, - tooltip: 'Record detailed logs of system events and user actions', - }, - ]; + const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = { + stable: [ + { + id: 'autoTemplate', + title: 'Auto Select Code Template', + description: 'Let Bolt select the best starter template for your project', + icon: 'i-ph:magic-wand', + enabled: autoSelectTemplate, + tooltip: 'Automatically choose the most suitable template based on your project type', + }, + { + id: 'contextOptimization', + title: 'Context Optimization', + description: 'Optimize chat context by redacting file contents and using system prompts', + icon: 'i-ph:arrows-in', + enabled: contextOptimizationEnabled, + tooltip: 'Improve AI responses by optimizing the context window and system prompts', + }, + { + id: 'eventLogs', + title: 'Event Logging', + description: 'Enable detailed event logging and history', + icon: 'i-ph:list-bullets', + enabled: eventLogs, + tooltip: 'Record detailed logs of system events and user actions', + }, + ], + beta: [ + { + id: 'latestBranch', + title: 'Use Main Branch', + description: 'Check for updates against the main branch instead of stable', + icon: 'i-ph:git-branch', + enabled: isLatestBranch, + beta: true, + tooltip: 'Get the latest features and improvements before they are officially released', + }, + ], + experimental: [ + { + id: 'experimentalProviders', + title: 'Experimental Providers', + description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike', + icon: 'i-ph:robot', + enabled: isLocalModel, + experimental: true, + tooltip: 'Try out new AI providers and models in development', + }, + ], + }; const handleToggleFeature = (featureId: string, enabled: boolean) => { switch (featureId) { @@ -107,163 +200,88 @@ export default function FeaturesTab() { }; return ( -
- -
-
-

Features

-

- Customize your Bolt experience with experimental features -

-
- - - - {features.map((feature, index) => ( - -
- {feature.beta && ( - - Beta - - )} - {feature.experimental && ( - - Experimental - - )} -
- -
- -
- - -
-
-
-

- {feature.title} -

-

{feature.description}

-
- handleToggleFeature(feature.id, checked)} - /> -
-
-
- - - - ))} -
+
+ + + {features.beta.length > 0 && ( + + )} + + {features.experimental.length > 0 && ( + + )} -
- +
-
- - -
-
-
-

- Prompt Library -

-

- Choose a prompt from the library to use as the system prompt -

-
- -
+
+
+

+ Prompt Library +

+

+ Choose a prompt from the library to use as the system prompt +

+
+
diff --git a/app/components/settings/notifications/NotificationsTab.tsx b/app/components/settings/notifications/NotificationsTab.tsx index 5291a4f0..8d154512 100644 --- a/app/components/settings/notifications/NotificationsTab.tsx +++ b/app/components/settings/notifications/NotificationsTab.tsx @@ -15,7 +15,7 @@ interface NotificationDetails { } const NotificationsTab = () => { - const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all'); + const [filter, setFilter] = useState<'all' | 'error' | 'warning' | 'update'>('all'); const logs = useStore(logStore.logs); const handleClearNotifications = () => { @@ -29,13 +29,44 @@ const NotificationsTab = () => { const filteredLogs = Object.values(logs) .filter((log) => { if (filter === 'all') { - return log.level === 'error' || log.level === 'warning'; + return log.level === 'error' || log.level === 'warning' || log.details?.type === 'update'; + } + + if (filter === 'update') { + return log.details?.type === 'update'; } return log.level === filter; }) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + const getNotificationStyle = (log: (typeof filteredLogs)[0]) => { + if (log.details?.type === 'update') { + return { + border: 'border-purple-200 dark:border-purple-900/50', + bg: 'bg-purple-50 dark:bg-purple-900/20', + icon: 'i-ph:arrow-circle-up text-purple-600 dark:text-purple-400', + text: 'text-purple-900 dark:text-purple-300', + }; + } + + if (log.level === 'error') { + return { + border: 'border-red-200 dark:border-red-900/50', + bg: 'bg-red-50 dark:bg-red-900/20', + icon: 'i-ph:warning-circle text-red-600 dark:text-red-400', + text: 'text-red-900 dark:text-red-300', + }; + } + + return { + border: 'border-yellow-200 dark:border-yellow-900/50', + bg: 'bg-yellow-50 dark:bg-yellow-900/20', + icon: 'i-ph:warning text-yellow-600 dark:text-yellow-400', + text: 'text-yellow-900 dark:text-yellow-300', + }; + }; + const renderNotificationDetails = (details: NotificationDetails) => { if (details.type === 'update') { return ( @@ -48,7 +79,7 @@ const NotificationsTab = () => {
) : ( - filteredLogs.map((log) => ( - -
-
- -
-

- {log.message} -

- {log.details && renderNotificationDetails(log.details as NotificationDetails)} + filteredLogs.map((log) => { + const style = getNotificationStyle(log); + return ( + +
+
+ +
+

{log.message}

+ {log.details && renderNotificationDetails(log.details as NotificationDetails)} +
+
- -
- - )) + + ); + }) )}
diff --git a/app/components/settings/settings.types.ts b/app/components/settings/settings.types.ts index cccadaf1..6f684cbc 100644 --- a/app/components/settings/settings.types.ts +++ b/app/components/settings/settings.types.ts @@ -17,6 +17,7 @@ export type TabType = export type WindowType = 'user' | 'developer'; export interface UserProfile { + nickname: any; name: string; email: string; avatar?: string; diff --git a/app/components/settings/update/UpdateTab.tsx b/app/components/settings/update/UpdateTab.tsx index 342bb0fc..db811fb6 100644 --- a/app/components/settings/update/UpdateTab.tsx +++ b/app/components/settings/update/UpdateTab.tsx @@ -1,11 +1,24 @@ import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { useSettings } from '~/lib/hooks/useSettings'; import { logStore } from '~/lib/stores/logs'; import { classNames } from '~/utils/classNames'; +import { toast } from 'react-toastify'; interface GitHubCommitResponse { sha: string; + commit: { + message: string; + }; +} + +interface GitHubReleaseResponse { + tag_name: string; + body: string; + assets: Array<{ + size: number; + browser_download_url: string; + }>; } interface UpdateInfo { @@ -13,26 +26,136 @@ interface UpdateInfo { latestVersion: string; branch: string; hasUpdate: boolean; + releaseNotes?: string; + downloadSize?: string; + changelog?: string[]; + currentCommit?: string; + latestCommit?: string; + downloadProgress?: number; + installProgress?: number; + estimatedTimeRemaining?: number; } -const GITHUB_URLS = { - commitJson: async (branch: string): Promise => { - try { - const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`); - const data = (await response.json()) as GitHubCommitResponse; +interface UpdateSettings { + autoUpdate: boolean; + notifyInApp: boolean; + checkInterval: number; +} - const currentCommitHash = __COMMIT_HASH; - const remoteCommitHash = data.sha.slice(0, 7); +interface UpdateResponse { + success: boolean; + error?: string; + progress?: { + downloaded: number; + total: number; + stage: 'download' | 'install' | 'complete'; + }; +} + +const categorizeChangelog = (messages: string[]) => { + const categories = new Map(); + + messages.forEach((message) => { + let category = 'Other'; + + if (message.startsWith('feat:')) { + category = 'Features'; + } else if (message.startsWith('fix:')) { + category = 'Bug Fixes'; + } else if (message.startsWith('docs:')) { + category = 'Documentation'; + } else if (message.startsWith('ci:')) { + category = 'CI Improvements'; + } else if (message.startsWith('refactor:')) { + category = 'Refactoring'; + } else if (message.startsWith('test:')) { + category = 'Testing'; + } else if (message.startsWith('style:')) { + category = 'Styling'; + } else if (message.startsWith('perf:')) { + category = 'Performance'; + } + + if (!categories.has(category)) { + categories.set(category, []); + } + + categories.get(category)!.push(message); + }); + + const order = [ + 'Features', + 'Bug Fixes', + 'Documentation', + 'CI Improvements', + 'Refactoring', + 'Performance', + 'Testing', + 'Styling', + 'Other', + ]; + + return Array.from(categories.entries()) + .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0])) + .filter(([_, messages]) => messages.length > 0); +}; + +const parseCommitMessage = (message: string) => { + const prMatch = message.match(/#(\d+)/); + const prNumber = prMatch ? prMatch[1] : null; + + let cleanMessage = message.replace(/^[a-z]+:\s*/i, ''); + cleanMessage = cleanMessage.replace(/#\d+/g, '').trim(); + + const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/); + const title = parts[0].trim(); + const description = parts + .slice(1) + .map((p) => p.trim()) + .filter((p) => p && !p.includes('Co-authored-by:')) + .join('\n'); + + return { title, description, prNumber }; +}; + +const GITHUB_URLS = { + commitJson: async (branch: string, headers: HeadersInit = {}): Promise => { + try { + const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([ + fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }), + fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }), + fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }), + ]); + + if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) { + throw new Error( + `GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`, + ); + } + + const commitData = (await commitResponse.json()) as GitHubCommitResponse; + const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse; + const commits = (await changelogResponse.json()) as GitHubCommitResponse[]; + + const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0; + const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB'; + + const changelog = commits.map((commit) => commit.commit.message); return { - currentVersion: currentCommitHash, - latestVersion: remoteCommitHash, + currentVersion: process.env.APP_VERSION || 'unknown', + latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7), branch, - hasUpdate: remoteCommitHash !== currentCommitHash, + hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT, + releaseNotes: releaseData.body || '', + downloadSize, + changelog, + currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7), + latestCommit: commitData.sha.substring(0, 7), }; } catch (error) { - console.error('Failed to fetch commit info:', error); - throw new Error('Failed to fetch commit info'); + console.error('Error fetching update info:', error); + throw error; } }, }; @@ -41,19 +164,71 @@ const UpdateTab = () => { const { isLatestBranch } = useSettings(); const [updateInfo, setUpdateInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [showChangelog, setShowChangelog] = useState(false); + const [showManualInstructions, setShowManualInstructions] = useState(false); + const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false); + const [updateFailed, setUpdateFailed] = useState(false); + const [updateSettings, setUpdateSettings] = useState(() => { + const stored = localStorage.getItem('update_settings'); + return stored + ? JSON.parse(stored) + : { + autoUpdate: false, + notifyInApp: true, + checkInterval: 24, + }; + }); + + useEffect(() => { + localStorage.setItem('update_settings', JSON.stringify(updateSettings)); + }, [updateSettings]); + + const handleUpdateProgress = async (response: Response): Promise => { + const reader = response.body?.getReader(); + + if (!reader) { + return; + } + + const contentLength = +(response.headers.get('Content-Length') ?? 0); + let receivedLength = 0; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + receivedLength += value.length; + + const progress = (receivedLength / contentLength) * 100; + + setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev)); + } + }; const checkForUpdates = async () => { setIsChecking(true); setError(null); try { + const githubToken = localStorage.getItem('github_connection'); + const headers: HeadersInit = {}; + + if (githubToken) { + const { token } = JSON.parse(githubToken); + headers.Authorization = `Bearer ${token}`; + } + const branchToCheck = isLatestBranch ? 'main' : 'stable'; - const info = await GITHUB_URLS.commitJson(branchToCheck); + const info = await GITHUB_URLS.commitJson(branchToCheck, headers); setUpdateInfo(info); if (info.hasUpdate) { - // Add update notification only if it doesn't already exist const existingLogs = Object.values(logStore.logs.get()); const hasUpdateNotification = existingLogs.some( (log) => @@ -62,7 +237,7 @@ const UpdateTab = () => { log.details.latestVersion === info.latestVersion, ); - if (!hasUpdateNotification) { + if (!hasUpdateNotification && updateSettings.notifyInApp) { logStore.logWarning('Update Available', { currentVersion: info.currentVersion, latestVersion: info.latestVersion, @@ -71,29 +246,123 @@ const UpdateTab = () => { message: `A new version is available on the ${branchToCheck} branch`, updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`, }); + + if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) { + const changelogText = info.changelog?.join('\n') || 'No changelog available'; + const userWantsUpdate = confirm( + `An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`, + ); + setHasUserRespondedToUpdate(true); + + if (userWantsUpdate) { + await initiateUpdate(); + } else { + logStore.logSystem('Update cancelled by user'); + } + } } } } catch (err) { setError('Failed to check for updates. Please try again later.'); console.error('Update check failed:', err); + setUpdateFailed(true); } finally { setIsChecking(false); } }; + const initiateUpdate = async () => { + setIsUpdating(true); + setError(null); + + let currentRetry = 0; + const maxRetries = 3; + + const attemptUpdate = async (): Promise => { + try { + const platform = process.platform; + + if (platform === 'darwin' || platform === 'linux') { + const response = await fetch('/api/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + branch: isLatestBranch ? 'main' : 'stable', + settings: updateSettings, + }), + }); + + if (!response.ok) { + throw new Error('Failed to initiate update'); + } + + await handleUpdateProgress(response); + + const result = (await response.json()) as UpdateResponse; + + if (result.success) { + logStore.logSuccess('Update downloaded successfully', { + type: 'update', + message: 'Update completed successfully.', + }); + toast.success('Update completed successfully!'); + setUpdateFailed(false); + + return; + } + + throw new Error(result.error || 'Update failed'); + } + + window.open('https://github.com/stackblitz-labs/bolt.diy/releases/latest', '_blank'); + logStore.logInfo('Manual update required', { + type: 'update', + message: 'Please download and install the latest version from the GitHub releases page.', + }); + + return; + } catch (err) { + currentRetry++; + + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + + if (currentRetry < maxRetries) { + toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 }); + setRetryCount(currentRetry); + await new Promise((resolve) => setTimeout(resolve, 2000)); + await attemptUpdate(); + + return; + } + + setError('Failed to initiate update. Please try again or update manually.'); + console.error('Update failed:', err); + logStore.logSystem('Update failed: ' + errorMessage); + toast.error('Update failed: ' + errorMessage); + setUpdateFailed(true); + + return; + } + }; + + await attemptUpdate(); + setIsUpdating(false); + setRetryCount(0); + }; + + useEffect(() => { + const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000; + const intervalId = setInterval(checkForUpdates, checkInterval); + + return () => clearInterval(intervalId); + }, [updateSettings.checkInterval, isLatestBranch]); + useEffect(() => { checkForUpdates(); }, [isLatestBranch]); - const handleViewChanges = () => { - if (updateInfo) { - window.open( - `https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`, - '_blank', - ); - } - }; - return (
{
+ {/* Update Settings Card */} -
+
+
+

Update Settings

+
+ +
+
+
+ Automatic Updates +

+ Automatically check and apply updates when available +

+
+ +
+ +
+
+ In-App Notifications +

Show notifications when updates are available

+
+ +
+ +
+
+ Check Interval +

How often to check for updates

+
+ +
+
+ + + {/* Update Status Card */} + +
Currently on {isLatestBranch ? 'main' : 'stable'} branch {updateInfo && ( - Version: {updateInfo.currentVersion} + + Version: {updateInfo.currentVersion} ({updateInfo.currentCommit}) + )}
{error && ( -
+
{error} @@ -156,60 +512,250 @@ const UpdateTab = () => { {updateInfo && (
-
-
- -
-

- {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'} -

-

- {updateInfo.hasUpdate - ? `A new version is available on the ${updateInfo.branch} branch` - : 'You are running the latest version'} -

- {updateInfo.hasUpdate && ( -
-

Current Version: {updateInfo.currentVersion}

-

Latest Version: {updateInfo.latestVersion}

-

Branch: {updateInfo.branch}

-
- )} -
+
+ +
+

+ {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'} +

+

+ {updateInfo.hasUpdate + ? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available` + : 'You are running the latest version'} +

- {updateInfo.hasUpdate && ( - - )}
)} + + {/* Update Details Card */} + {updateInfo && updateInfo.hasUpdate && ( + +
+
+
+ + Version {updateInfo.latestVersion} + +
+ + {updateInfo.downloadSize} + +
+ + {/* Update Options */} +
+
+ + +
+ + {/* Manual Update Instructions */} + + {showManualInstructions && ( + +
+

+ Update available from {isLatestBranch ? 'main' : 'stable'} branch! +

+
+

+ Current: {updateInfo.currentVersion} ({updateInfo.currentCommit}) +

+

+ Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit}) +

+
+
+ +
+

To update:

+
    +
  1. +
    + 1 +
    +
    +

    Pull the latest changes:

    + + git pull upstream {isLatestBranch ? 'main' : 'stable'} + +
    +
  2. +
  3. +
    + 2 +
    +
    +

    Install dependencies:

    + + pnpm install + +
    +
  4. +
  5. +
    + 3 +
    +
    +

    Build the application:

    + + pnpm build + +
    +
  6. +
  7. +
    + 4 +
    +

    Restart the application

    +
  8. +
+
+
+ )} +
+ + {/* Changelog */} + {updateInfo.changelog && updateInfo.changelog.length > 0 && ( +
+ + + + {showChangelog && ( + +
+ {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => ( +
+
+
+ {category} + + ({messages.length}) + +
+
+
+ {messages.map((message, index) => { + const { title, description, prNumber } = parseCommitMessage(message); + return ( +
+
+
+
+

+ {title} + {prNumber && ( + + #{prNumber} + + )} +

+ {description && ( +

{description}

+ )} +
+
+
+ ); + })} +
+
+ ))} +
+ + )} + +
+ )} +
+ + )} + + {/* Update Progress */} + {isUpdating && updateInfo?.downloadProgress !== undefined && ( + +
+
+ Downloading Update + + {Math.round(updateInfo.downloadProgress)}% + +
+
+
+
+ {retryCount > 0 &&

Retry attempt {retryCount}/3...

} +
+ + )}
); }; diff --git a/app/components/settings/user/UsersWindow.tsx b/app/components/settings/user/UsersWindow.tsx index 4d0e799c..f2e33311 100644 --- a/app/components/settings/user/UsersWindow.tsx +++ b/app/components/settings/user/UsersWindow.tsx @@ -1,6 +1,7 @@ import * as RadixDialog from '@radix-ui/react-dialog'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { motion } from 'framer-motion'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { classNames } from '~/utils/classNames'; import { DialogTitle } from '~/components/ui/Dialog'; import { Switch } from '~/components/ui/Switch'; @@ -117,6 +118,24 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus(); const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus(); + const [profile, setProfile] = useState(() => { + const saved = localStorage.getItem('bolt_user_profile'); + return saved ? JSON.parse(saved) : { avatar: null, notifications: true }; + }); + + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'bolt_user_profile') { + const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true }; + setProfile(newProfile); + } + }; + + window.addEventListener('storage', handleStorageChange); + + return () => window.removeEventListener('storage', handleStorageChange); + }, []); + const handleDeveloperModeChange = (checked: boolean) => { setDeveloperMode(checked); }; @@ -127,7 +146,14 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { // Only show tabs that are assigned to the user window AND are visible const visibleUserTabs = tabConfiguration.userTabs - .filter((tab: TabVisibilityConfig) => tab.window === 'user' && tab.visible) + .filter((tab) => { + // Hide notifications tab if notifications are disabled + if (tab.id === 'notifications' && !profile.notifications) { + return false; + } + + return tab.visible; + }) .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0)); const moveTab = (dragIndex: number, hoverIndex: number) => { @@ -240,6 +266,142 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { } }; + const renderHeader = () => ( +
+
+ {activeTab ? ( + + ) : ( + + )} + + {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'} + +
+ +
+
+ + +
+ + + + + + + + + handleTabClick('profile')} + > +
+
+
+ Profile + + + handleTabClick('settings')} + > +
+
+
+ Settings + + + {profile.notifications && ( + <> + handleTabClick('notifications')} + > +
+
+
+ + Notifications + {hasUnreadNotifications && ( + + {unreadNotifications.length} + + )} + + + + + + )} + + +
+
+
+ Close + + + + + + +
+
+ ); + return ( <> setDeveloperMode(false)} /> @@ -273,64 +435,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { transition={{ duration: 0.2 }} > {/* Header */} -
-
- {activeTab ? ( - -
- - ) : ( - - )} - - {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'} - -
-
-
- - -
- -
- -
-
+ {renderHeader()} {/* Content */}
{ + const type: NotificationType = + log.details?.type === 'update' + ? 'update' + : log.level === 'error' + ? 'error' + : log.level === 'warning' + ? 'warning' + : 'info'; + + const baseNotification: Notification = { + id: log.id, + title: log.category.charAt(0).toUpperCase() + log.category.slice(1), + message: log.message, + type, + read: log.read || false, + timestamp: log.timestamp, + }; + + if (log.details) { + return { + ...baseNotification, + details: log.details as NotificationDetails, + }; + } + + return baseNotification; +}; + export const getNotifications = async (): Promise => { - /* - * TODO: Implement actual notifications logic - * This is a mock implementation - */ - return [ - { - id: 'notif-1', - title: 'Welcome to Bolt', - message: 'Get started by exploring the features', - type: 'info', - read: true, - timestamp: new Date().toISOString(), - }, - { - id: 'notif-2', - title: 'New Update Available', - message: 'Version 1.0.1 is now available', - type: 'info', - read: false, - timestamp: new Date().toISOString(), - }, - ]; + const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[]; + + return logs + .filter((log) => { + if (log.details?.type === 'update') { + return true; + } + + return log.level === 'error' || log.level === 'warning'; + }) + .map(mapLogToNotification) + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); }; export const markNotificationRead = async (notificationId: string): Promise => { - /* - * TODO: Implement actual notification read logic - */ - console.log(`Marking notification ${notificationId} as read`); + logStore.markAsRead(notificationId); +}; + +export const clearNotifications = async (): Promise => { + logStore.clearLogs(); +}; + +export const getUnreadCount = (): number => { + const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[]; + + return logs.filter((log) => { + if (!log.read) { + if (log.details?.type === 'update') { + return true; + } + + return log.level === 'error' || log.level === 'warning'; + } + + return false; + }).length; }; diff --git a/app/lib/hooks/useNotifications.ts b/app/lib/hooks/useNotifications.ts index 00aaee39..151dd39e 100644 --- a/app/lib/hooks/useNotifications.ts +++ b/app/lib/hooks/useNotifications.ts @@ -1,34 +1,17 @@ import { useState, useEffect } from 'react'; import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications'; - -const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications'; - -const getReadNotifications = (): string[] => { - try { - const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } -}; - -const setReadNotifications = (notificationIds: string[]) => { - try { - localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds)); - } catch (error) { - console.error('Failed to persist read notifications:', error); - } -}; +import { logStore } from '~/lib/stores/logs'; +import { useStore } from '@nanostores/react'; export const useNotifications = () => { const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); const [unreadNotifications, setUnreadNotifications] = useState([]); - const [readNotificationIds, setReadNotificationIds] = useState(() => getReadNotifications()); + const logs = useStore(logStore.logs); const checkNotifications = async () => { try { const notifications = await getNotifications(); - const unread = notifications.filter((n) => !readNotificationIds.includes(n.id)); + const unread = notifications.filter((n) => !logStore.isRead(n.id)); setUnreadNotifications(unread); setHasUnreadNotifications(unread.length > 0); } catch (error) { @@ -43,17 +26,12 @@ export const useNotifications = () => { const interval = setInterval(checkNotifications, 60 * 1000); return () => clearInterval(interval); - }, [readNotificationIds]); + }, [logs]); // Re-run when logs change const markAsRead = async (notificationId: string) => { try { await markNotificationRead(notificationId); - - const newReadIds = [...readNotificationIds, notificationId]; - setReadNotificationIds(newReadIds); - setReadNotifications(newReadIds); - setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId)); - setHasUnreadNotifications(unreadNotifications.length > 1); + await checkNotifications(); } catch (error) { console.error('Failed to mark notification as read:', error); } @@ -61,13 +39,9 @@ export const useNotifications = () => { const markAllAsRead = async () => { try { - await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id))); - - const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)]; - setReadNotificationIds(newReadIds); - setReadNotifications(newReadIds); - setUnreadNotifications([]); - setHasUnreadNotifications(false); + const notifications = await getNotifications(); + await Promise.all(notifications.map((n) => markNotificationRead(n.id))); + await checkNotifications(); } catch (error) { console.error('Failed to mark all notifications as read:', error); } diff --git a/app/lib/stores/logs.ts b/app/lib/stores/logs.ts index 1992a4d1..d36caff9 100644 --- a/app/lib/stores/logs.ts +++ b/app/lib/stores/logs.ts @@ -19,12 +19,25 @@ export interface LogEntry { const MAX_LOGS = 1000; // Maximum number of logs to keep in memory class LogStore { + logInfo(message: string, details: { type: string; message: string }) { + return this.addLog(message, 'info', 'system', details); + } + + logSuccess(message: string, details: { type: string; message: string }) { + return this.addLog(message, 'info', 'system', { ...details, success: true }); + } private _logs = map>({}); showLogs = atom(true); + private _readLogs = new Set(); constructor() { // Load saved logs from cookies on initialization this._loadLogs(); + + // Only load read logs in browser environment + if (typeof window !== 'undefined') { + this._loadReadLogs(); + } } // Expose the logs store for subscription @@ -45,11 +58,36 @@ class LogStore { } } + private _loadReadLogs() { + if (typeof window === 'undefined') { + return; + } + + const savedReadLogs = localStorage.getItem('bolt_read_logs'); + + if (savedReadLogs) { + try { + const parsedReadLogs = JSON.parse(savedReadLogs); + this._readLogs = new Set(parsedReadLogs); + } catch (error) { + logger.error('Failed to parse read logs:', error); + } + } + } + private _saveLogs() { const currentLogs = this._logs.get(); Cookies.set('eventLogs', JSON.stringify(currentLogs)); } + private _saveReadLogs() { + if (typeof window === 'undefined') { + return; + } + + localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs))); + } + private _generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } @@ -210,6 +248,20 @@ class LogStore { return matchesLevel && matchesCategory && matchesSearch; }); } + + markAsRead(logId: string) { + this._readLogs.add(logId); + this._saveReadLogs(); + } + + isRead(logId: string): boolean { + return this._readLogs.has(logId); + } + + clearReadLogs() { + this._readLogs.clear(); + this._saveReadLogs(); + } } export const logStore = new LogStore(); diff --git a/package.json b/package.json index e9877f93..e8b4003c 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "js-cookie": "^3.0.5", "jszip": "^3.10.1", "nanostores": "^0.10.3", + "next": "^15.1.5", "ollama-ai-provider": "^0.15.2", "react": "^18.3.1", "react-dnd": "^16.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a37309..9edd7588 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,6 +191,9 @@ importers: nanostores: specifier: ^0.10.3 version: 0.10.3 + next: + specifier: ^15.1.5 + version: 15.1.5(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ollama-ai-provider: specifier: ^0.15.2 version: 0.15.2(zod@3.23.8) @@ -849,6 +852,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} @@ -1506,6 +1512,111 @@ packages: '@iconify/utils@2.1.33': resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1577,6 +1688,57 @@ packages: nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 react: '>=18.0.0' + '@next/env@15.1.5': + resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==} + + '@next/swc-darwin-arm64@15.1.5': + resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.1.5': + resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.1.5': + resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.1.5': + resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.1.5': + resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.1.5': + resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.1.5': + resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.1.5': + resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2475,6 +2637,9 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2991,6 +3156,10 @@ packages: peerDependencies: esbuild: '>=0.18' + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3104,6 +3273,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -3305,6 +3481,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -3961,6 +4141,9 @@ packages: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -4650,6 +4833,27 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + next@15.1.5: + resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -4942,6 +5146,10 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} @@ -5496,6 +5704,10 @@ packages: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5527,6 +5739,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -5598,6 +5813,10 @@ packages: stream-slice@0.1.2: resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -5650,6 +5869,19 @@ packages: style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -7161,6 +7393,11 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/hash@0.9.2': {} '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': @@ -7556,6 +7793,81 @@ snapshots: transitivePeerDependencies: - supports-color + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7673,6 +7985,32 @@ snapshots: nanostores: 0.10.3 react: 18.3.1 + '@next/env@15.1.5': {} + + '@next/swc-darwin-arm64@15.1.5': + optional: true + + '@next/swc-darwin-x64@15.1.5': + optional: true + + '@next/swc-linux-arm64-gnu@15.1.5': + optional: true + + '@next/swc-linux-arm64-musl@15.1.5': + optional: true + + '@next/swc-linux-x64-gnu@15.1.5': + optional: true + + '@next/swc-linux-x64-musl@15.1.5': + optional: true + + '@next/swc-win32-arm64-msvc@15.1.5': + optional: true + + '@next/swc-win32-x64-msvc@15.1.5': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8739,6 +9077,8 @@ snapshots: - supports-color - typescript + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -9432,6 +9772,10 @@ snapshots: esbuild: 0.23.1 load-tsconfig: 0.2.5 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -9546,6 +9890,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + colorette@2.0.20: {} colorjs.io@0.5.2: {} @@ -9711,6 +10067,9 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.3: + optional: true + detect-node-es@1.1.0: {} devlop@1.1.0: @@ -10569,6 +10928,9 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-arrayish@0.3.2: + optional: true + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -11605,6 +11967,32 @@ snapshots: negotiator@0.6.3: {} + next@15.1.5(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 15.1.5 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001685 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.1.5 + '@next/swc-darwin-x64': 15.1.5 + '@next/swc-linux-arm64-gnu': 15.1.5 + '@next/swc-linux-arm64-musl': 15.1.5 + '@next/swc-linux-x64-gnu': 15.1.5 + '@next/swc-linux-x64-musl': 15.1.5 + '@next/swc-win32-arm64-msvc': 15.1.5 + '@next/swc-win32-x64-msvc': 15.1.5 + '@opentelemetry/api': 1.9.0 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + node-domexception@1.0.0: {} node-fetch-native@1.6.4: {} @@ -11929,6 +12317,12 @@ snapshots: postcss-value-parser@4.2.0: {} + postcss@8.4.31: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.4.49: dependencies: nanoid: 3.3.8 @@ -12512,6 +12906,33 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -12548,6 +12969,11 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.28 @@ -12616,6 +13042,8 @@ snapshots: stream-slice@0.1.2: {} + streamsearch@1.1.0: {} + string-hash@1.1.3: {} string-width@4.2.3: @@ -12669,6 +13097,13 @@ snapshots: dependencies: inline-style-parser: 0.2.4 + styled-jsx@5.1.6(@babel/core@7.26.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.26.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 00000000..cb570e15 --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Exit on any error +set -e + +echo "Starting Bolt.DIY update process..." + +# Get the current directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Store current version +CURRENT_VERSION=$(cat "$PROJECT_ROOT/package.json" | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') + +echo "Current version: $CURRENT_VERSION" +echo "Fetching latest version..." + +# Create temp directory +TMP_DIR=$(mktemp -d) +cd "$TMP_DIR" + +# Download latest release +LATEST_RELEASE_URL=$(curl -s https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d \") +if [ -z "$LATEST_RELEASE_URL" ]; then + echo "Error: Could not find latest release download URL" + exit 1 +fi + +echo "Downloading latest release..." +curl -L -o latest.zip "$LATEST_RELEASE_URL" + +echo "Extracting update..." +unzip -q latest.zip + +# Backup current installation +echo "Creating backup..." +BACKUP_DIR="$PROJECT_ROOT/backup_$(date +%Y%m%d_%H%M%S)" +mkdir -p "$BACKUP_DIR" +cp -r "$PROJECT_ROOT"/* "$BACKUP_DIR/" + +# Install update +echo "Installing update..." +cp -r ./* "$PROJECT_ROOT/" + +# Clean up +cd "$PROJECT_ROOT" +rm -rf "$TMP_DIR" + +echo "Update completed successfully!" +echo "Please restart the application to apply the changes." + +exit 0