Add native runtime update notifications
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
name: Runtime release watch
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '17 */6 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
php-releases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Check official PHP releases
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
RESULT=$(node scripts/check-php-releases.mjs --report "$RUNNER_TEMP/php-runtime-report.md")
|
||||||
|
echo "count=$(node -e 'const value=JSON.parse(process.argv[1]); console.log(value.updates.length)' "$RESULT")" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Create or refresh packaging issue
|
||||||
|
if: steps.check.outputs.count != '0'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TITLE="Runtime update: new PHP release needs packaging"
|
||||||
|
EXISTING=$(gh issue list --state open --search "$TITLE in:title" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
gh issue edit "$EXISTING" --body-file "$RUNNER_TEMP/php-runtime-report.md"
|
||||||
|
else
|
||||||
|
gh issue create --title "$TITLE" --body-file "$RUNNER_TEMP/php-runtime-report.md"
|
||||||
|
fi
|
||||||
@@ -26,3 +26,9 @@ Every project receives reserved loopback ports, generated service configuration,
|
|||||||
6. Additional PHP/database versions, Apache, Drupal, Node.js, and developer services.
|
6. Additional PHP/database versions, Apache, Drupal, Node.js, and developer services.
|
||||||
|
|
||||||
Native project creation must stay disabled until the platform bundle passes executable, service-health, database, routing, and cleanup checks. Existing projects default to the container engine for backward compatibility.
|
Native project creation must stay disabled until the platform bundle passes executable, service-health, database, routing, and cleanup checks. Existing projects default to the container engine for backward compatibility.
|
||||||
|
|
||||||
|
## Runtime update notifications
|
||||||
|
|
||||||
|
Dockside reads the bundled runtime catalog at startup and checks again every six hours. A remote catalog is accepted only when `AURORA_RUNTIME_CATALOG_PUBLIC_KEY` contains the Ed25519 public key and the adjacent `catalog.json.sig` validates. The last verified remote catalog is cached; otherwise Dockside falls back to its bundled trusted catalog.
|
||||||
|
|
||||||
|
The scheduled `runtime-release-watch.yml` workflow checks PHP's official JSON release feed every six hours. When a tracked PHP branch changes, it creates or updates a GitHub issue. It does not publish a runtime automatically: each platform package must be built, checksummed, tested, added to the catalog, and signed before users see it.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"build:modules": "node scripts/package-modules.cjs",
|
"build:modules": "node scripts/package-modules.cjs",
|
||||||
"build:connector": "node scripts/package-connector.cjs",
|
"build:connector": "node scripts/package-connector.cjs",
|
||||||
"build:native-runtime": "node scripts/package-native-runtime.cjs",
|
"build:native-runtime": "node scripts/package-native-runtime.cjs",
|
||||||
|
"check:php-releases": "node scripts/check-php-releases.mjs",
|
||||||
"module:new": "node scripts/create-module.cjs",
|
"module:new": "node scripts/create-module.cjs",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"generatedAt": "2026-08-22T00:00:00.000Z",
|
||||||
|
"releases": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"checkedAt": "2026-08-22T00:00:00.000Z",
|
||||||
|
"php": {
|
||||||
|
"8.2": "8.2.33",
|
||||||
|
"8.3": "8.3.33",
|
||||||
|
"8.4": "8.4.24",
|
||||||
|
"8.5": "8.5.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
|
const sourceUrl = 'https://www.php.net/releases/index.php?json&version=8&max=30'
|
||||||
|
const baselinePath = resolve('runtime/upstream-baseline.json')
|
||||||
|
const reportArgument = process.argv.indexOf('--report')
|
||||||
|
const reportPath = reportArgument >= 0 ? process.argv[reportArgument + 1] : undefined
|
||||||
|
|
||||||
|
function latestByBranch(releases) {
|
||||||
|
const result = {}
|
||||||
|
for (const version of Object.keys(releases)) {
|
||||||
|
if (!/^8\.[2-5]\.\d+$/.test(version)) continue
|
||||||
|
const branch = version.split('.').slice(0, 2).join('.')
|
||||||
|
if (!result[branch] || Number(version.split('.')[2]) > Number(result[branch].split('.')[2])) result[branch] = version
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseline = JSON.parse(await readFile(baselinePath, 'utf8'))
|
||||||
|
const response = await fetch(sourceUrl, { headers: { 'user-agent': 'Aurora-Dockside-Runtime-Watcher/1.0' } })
|
||||||
|
if (!response.ok) throw new Error(`PHP release feed returned HTTP ${response.status}.`)
|
||||||
|
const releases = await response.json()
|
||||||
|
const latest = latestByBranch(releases)
|
||||||
|
const updates = Object.entries(latest)
|
||||||
|
.filter(([branch, version]) => baseline.php[branch] !== version)
|
||||||
|
.map(([branch, version]) => ({ branch, packaged: baseline.php[branch] || 'not tracked', upstream: version, security: releases[version]?.tags?.includes('security') === true }))
|
||||||
|
|
||||||
|
const lines = updates.length
|
||||||
|
? ['# PHP runtime releases need packaging', '', 'The official PHP release feed contains versions newer than Aurora’s tracked packaging baseline.', '', '| Branch | Aurora baseline | Upstream | Security release |', '| --- | --- | --- | --- |', ...updates.map((update) => `| ${update.branch} | ${update.packaged} | ${update.upstream} | ${update.security ? 'Yes' : 'No'} |`), '', `Source: ${sourceUrl}`, '', 'After packages pass platform tests, update `runtime/upstream-baseline.json` and the signed runtime catalog.']
|
||||||
|
: ['# PHP runtime release check', '', 'No untracked PHP releases were found.']
|
||||||
|
|
||||||
|
if (reportPath) await writeFile(reportPath, `${lines.join('\n')}\n`, 'utf8')
|
||||||
|
process.stdout.write(`${JSON.stringify({ updates, latest })}\n`)
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { getRuntimeStatus } from '../nativeRuntime'
|
import { getRuntimeStatus } from '../nativeRuntime'
|
||||||
|
import { getRuntimeUpdates } from '../runtimeCatalog'
|
||||||
|
|
||||||
export function registerRuntimeIpc(): void {
|
export function registerRuntimeIpc(): void {
|
||||||
ipcMain.handle('runtime:status', () => getRuntimeStatus())
|
ipcMain.handle('runtime:status', () => getRuntimeStatus())
|
||||||
|
ipcMain.handle('runtime:updates', async () => {
|
||||||
|
const status = await getRuntimeStatus()
|
||||||
|
return getRuntimeUpdates(status.native.components)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { findRuntimeUpdates, validateRuntimeCatalog } from './runtimeCatalog'
|
||||||
|
|
||||||
|
const checksum = 'a'.repeat(64)
|
||||||
|
|
||||||
|
describe('runtime catalog', () => {
|
||||||
|
it('selects the newest compatible component release', () => {
|
||||||
|
const catalog = validateRuntimeCatalog({ schema: 1, generatedAt: '2026-08-22T00:00:00.000Z', releases: [
|
||||||
|
{ component: 'php', version: '8.5.8', platform: 'linux', arch: 'x64', channel: 'stable', url: 'https://auroradockside.com/php-8.5.8.tar.zst', sha256: checksum },
|
||||||
|
{ component: 'php', version: '8.5.9', platform: 'linux', arch: 'x64', channel: 'security', url: 'https://auroradockside.com/php-8.5.9.tar.zst', sha256: checksum },
|
||||||
|
{ component: 'php', version: '8.6.0', platform: 'darwin', arch: 'arm64', channel: 'stable', url: 'https://auroradockside.com/php-8.6.0.tar.zst', sha256: checksum }
|
||||||
|
] })
|
||||||
|
expect(findRuntimeUpdates(catalog, [{ id: 'php', version: '8.5.7', executable: 'bin/php', sha256: checksum }], 'linux', 'x64')).toEqual([
|
||||||
|
{ component: 'php', installedVersion: '8.5.7', availableVersion: '8.5.9', channel: 'security' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects insecure release URLs', () => {
|
||||||
|
expect(() => validateRuntimeCatalog({ schema: 1, generatedAt: 'now', releases: [{ component: 'php', version: '8.5.9', platform: 'linux', arch: 'x64', channel: 'stable', url: 'http://example.test/php', sha256: checksum }] })).toThrow('Invalid runtime catalog release')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { createPublicKey, verify } from 'crypto'
|
||||||
|
import { mkdir, readFile, writeFile } from 'fs/promises'
|
||||||
|
import { join } from 'path'
|
||||||
|
import type {
|
||||||
|
AuroraNativeRuntimeComponent,
|
||||||
|
AuroraRuntimeCatalog,
|
||||||
|
AuroraRuntimeCatalogRelease,
|
||||||
|
AuroraRuntimeUpdate,
|
||||||
|
AuroraRuntimeUpdateStatus
|
||||||
|
} from '../shared/types'
|
||||||
|
|
||||||
|
const catalogUrl = process.env.AURORA_RUNTIME_CATALOG_URL || 'https://auroradockside.com/runtime/catalog.json'
|
||||||
|
const publicKeyPem = process.env.AURORA_RUNTIME_CATALOG_PUBLIC_KEY
|
||||||
|
|
||||||
|
function compareVersions(left: string, right: string): number {
|
||||||
|
const a = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0)
|
||||||
|
const b = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0)
|
||||||
|
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
||||||
|
if ((a[index] || 0) !== (b[index] || 0)) return (a[index] || 0) - (b[index] || 0)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRuntimeCatalog(value: unknown): AuroraRuntimeCatalog {
|
||||||
|
if (!value || typeof value !== 'object') throw new Error('Runtime catalog must be an object.')
|
||||||
|
const catalog = value as Record<string, unknown>
|
||||||
|
if (catalog.schema !== 1 || typeof catalog.generatedAt !== 'string' || !Array.isArray(catalog.releases))
|
||||||
|
throw new Error('Unsupported runtime catalog.')
|
||||||
|
const releases = catalog.releases.map((entry) => {
|
||||||
|
if (!entry || typeof entry !== 'object') throw new Error('Invalid runtime catalog release.')
|
||||||
|
const release = entry as Record<string, unknown>
|
||||||
|
if (
|
||||||
|
!['php', 'nginx', 'apache', 'mariadb', 'mysql', 'postgres', 'node', 'composer', 'wp-cli', 'drush'].includes(String(release.component)) ||
|
||||||
|
typeof release.version !== 'string' ||
|
||||||
|
!['linux', 'darwin', 'win32'].includes(String(release.platform)) ||
|
||||||
|
!['x64', 'arm64'].includes(String(release.arch)) ||
|
||||||
|
!['stable', 'security'].includes(String(release.channel)) ||
|
||||||
|
typeof release.url !== 'string' ||
|
||||||
|
!release.url.startsWith('https://') ||
|
||||||
|
typeof release.sha256 !== 'string' ||
|
||||||
|
!/^[a-f0-9]{64}$/i.test(release.sha256)
|
||||||
|
) throw new Error('Invalid runtime catalog release.')
|
||||||
|
return release as unknown as AuroraRuntimeCatalogRelease
|
||||||
|
})
|
||||||
|
return { schema: 1, generatedAt: catalog.generatedAt, releases }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findRuntimeUpdates(
|
||||||
|
catalog: AuroraRuntimeCatalog,
|
||||||
|
installed: AuroraNativeRuntimeComponent[],
|
||||||
|
platform: NodeJS.Platform,
|
||||||
|
arch: string
|
||||||
|
): AuroraRuntimeUpdate[] {
|
||||||
|
return installed.flatMap((component) => {
|
||||||
|
const candidates = catalog.releases.filter((release) =>
|
||||||
|
release.component === component.id && release.platform === platform && release.arch === arch
|
||||||
|
)
|
||||||
|
const latest = candidates.sort((a, b) => compareVersions(b.version, a.version))[0]
|
||||||
|
return latest && compareVersions(latest.version, component.version) > 0
|
||||||
|
? [{ component: component.id, installedVersion: component.version, availableVersion: latest.version, channel: latest.channel }]
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundledCatalogPath(): string {
|
||||||
|
return join(app.getAppPath(), 'resources', 'runtime-catalog', 'catalog.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remoteCatalog(): Promise<AuroraRuntimeCatalog> {
|
||||||
|
if (!publicKeyPem) throw new Error('Remote runtime catalog verification is not configured yet.')
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timer = setTimeout(() => controller.abort(), 8000)
|
||||||
|
try {
|
||||||
|
const [catalogResponse, signatureResponse] = await Promise.all([
|
||||||
|
fetch(catalogUrl, { signal: controller.signal }),
|
||||||
|
fetch(`${catalogUrl}.sig`, { signal: controller.signal })
|
||||||
|
])
|
||||||
|
if (!catalogResponse.ok || !signatureResponse.ok) throw new Error('Runtime catalog server is unavailable.')
|
||||||
|
const body = Buffer.from(await catalogResponse.arrayBuffer())
|
||||||
|
const signature = Buffer.from((await signatureResponse.text()).trim(), 'base64')
|
||||||
|
if (!verify(null, body, createPublicKey(publicKeyPem), signature))
|
||||||
|
throw new Error('Runtime catalog signature is invalid.')
|
||||||
|
return validateRuntimeCatalog(JSON.parse(body.toString('utf8')))
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRuntimeUpdates(installed: AuroraNativeRuntimeComponent[]): Promise<AuroraRuntimeUpdateStatus> {
|
||||||
|
const cacheDirectory = join(app.getPath('userData'), 'runtime-catalog')
|
||||||
|
const cachePath = join(cacheDirectory, 'catalog.json')
|
||||||
|
let source: AuroraRuntimeUpdateStatus['source'] = 'bundled'
|
||||||
|
let message: string | undefined
|
||||||
|
let catalog: AuroraRuntimeCatalog
|
||||||
|
try {
|
||||||
|
catalog = await remoteCatalog()
|
||||||
|
source = 'remote'
|
||||||
|
await mkdir(cacheDirectory, { recursive: true })
|
||||||
|
await writeFile(cachePath, JSON.stringify(catalog, null, 2), 'utf8')
|
||||||
|
} catch (error) {
|
||||||
|
message = error instanceof Error ? error.message : 'Runtime update check failed.'
|
||||||
|
try {
|
||||||
|
catalog = validateRuntimeCatalog(JSON.parse(await readFile(cachePath, 'utf8')))
|
||||||
|
source = 'cache'
|
||||||
|
} catch {
|
||||||
|
catalog = validateRuntimeCatalog(JSON.parse(await readFile(bundledCatalogPath(), 'utf8')))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
source,
|
||||||
|
updates: findRuntimeUpdates(catalog, installed, process.platform, process.arch),
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
AuroraRemoteSiteProfile,
|
AuroraRemoteSiteProfile,
|
||||||
AuroraRemoteSiteStatus,
|
AuroraRemoteSiteStatus,
|
||||||
AuroraRuntimeStatus,
|
AuroraRuntimeStatus,
|
||||||
|
AuroraRuntimeUpdateStatus,
|
||||||
AuroraStackOptions,
|
AuroraStackOptions,
|
||||||
EnvironmentUpdate,
|
EnvironmentUpdate,
|
||||||
LogDataEvent,
|
LogDataEvent,
|
||||||
@@ -157,7 +158,8 @@ const api = {
|
|||||||
ipcRenderer.invoke('remote:pull', operationId, name)
|
ipcRenderer.invoke('remote:pull', operationId, name)
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
status: (): Promise<AuroraRuntimeStatus> => ipcRenderer.invoke('runtime:status')
|
status: (): Promise<AuroraRuntimeStatus> => ipcRenderer.invoke('runtime:status'),
|
||||||
|
updates: (): Promise<AuroraRuntimeUpdateStatus> => ipcRenderer.invoke('runtime:updates')
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
in: (): Promise<number> => ipcRenderer.invoke('window:zoomIn'),
|
in: (): Promise<number> => ipcRenderer.invoke('window:zoomIn'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { Anchor, Boxes, FolderOpen, Plus, Settings, Sparkles, TerminalSquare } from 'lucide-react'
|
import { Anchor, Bell, Boxes, FolderOpen, Plus, Settings, Sparkles, TerminalSquare } from 'lucide-react'
|
||||||
import { ProjectDetail } from './components/projects/ProjectDetail'
|
import { ProjectDetail } from './components/projects/ProjectDetail'
|
||||||
import { ProjectList } from './components/projects/ProjectList'
|
import { ProjectList } from './components/projects/ProjectList'
|
||||||
import { TerminalPanel } from './components/terminal/TerminalPanel'
|
import { TerminalPanel } from './components/terminal/TerminalPanel'
|
||||||
@@ -13,12 +13,14 @@ import { useTerminalEvents } from './hooks/useTerminalEvents'
|
|||||||
import { useAppliedTheme } from './hooks/useAppliedTheme'
|
import { useAppliedTheme } from './hooks/useAppliedTheme'
|
||||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||||
import docksideIcon from './assets/dockside-icon.png'
|
import docksideIcon from './assets/dockside-icon.png'
|
||||||
|
import { useRuntimeUpdates } from './hooks/useRuntime'
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
const selectedProjectName = useAppStore((s) => s.selectedProjectName)
|
const selectedProjectName = useAppStore((s) => s.selectedProjectName)
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false)
|
const [isCreateOpen, setIsCreateOpen] = useState(false)
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||||
const [isModulesOpen, setIsModulesOpen] = useState(false)
|
const [isModulesOpen, setIsModulesOpen] = useState(false)
|
||||||
|
const { data: runtimeUpdates } = useRuntimeUpdates()
|
||||||
useTerminalEvents()
|
useTerminalEvents()
|
||||||
useAppliedTheme()
|
useAppliedTheme()
|
||||||
useKeyboardShortcuts({
|
useKeyboardShortcuts({
|
||||||
@@ -53,6 +55,17 @@ function App(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
{runtimeUpdates && runtimeUpdates.updates.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
title={`${runtimeUpdates.updates.length} runtime update${runtimeUpdates.updates.length === 1 ? '' : 's'} available`}
|
||||||
|
className="relative rounded-md p-1.5 text-amber-600 transition hover:bg-amber-50 dark:text-amber-300 dark:hover:bg-amber-400/10"
|
||||||
|
>
|
||||||
|
<Bell size={16} />
|
||||||
|
<span className="absolute right-0 top-0 size-2 rounded-full bg-amber-500" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsSettingsOpen(true)}
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Minus, Plus, RotateCcw, X } from 'lucide-react'
|
import { CheckCircle2, Minus, Plus, RefreshCw, RotateCcw, X } from 'lucide-react'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useThemeStore, type Theme } from '../../stores/themeStore'
|
import { useThemeStore, type Theme } from '../../stores/themeStore'
|
||||||
|
import { useRuntimeUpdates } from '../../hooks/useRuntime'
|
||||||
|
|
||||||
const THEMES: { value: Theme; label: string }[] = [
|
const THEMES: { value: Theme; label: string }[] = [
|
||||||
{ value: 'light', label: 'Light' },
|
{ value: 'light', label: 'Light' },
|
||||||
@@ -18,6 +19,7 @@ const SHORTCUTS = [
|
|||||||
export function SettingsModal({ onClose }: { onClose: () => void }): React.JSX.Element {
|
export function SettingsModal({ onClose }: { onClose: () => void }): React.JSX.Element {
|
||||||
const theme = useThemeStore((s) => s.theme)
|
const theme = useThemeStore((s) => s.theme)
|
||||||
const setTheme = useThemeStore((s) => s.setTheme)
|
const setTheme = useThemeStore((s) => s.setTheme)
|
||||||
|
const runtimeUpdates = useRuntimeUpdates()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-8">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-8">
|
||||||
@@ -34,6 +36,20 @@ export function SettingsModal({ onClose }: { onClose: () => void }): React.JSX.E
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-6 p-4">
|
<div className="flex flex-col gap-6 p-4">
|
||||||
|
<section>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h3 className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Runtime updates</h3>
|
||||||
|
<button type="button" onClick={() => runtimeUpdates.refetch()} disabled={runtimeUpdates.isFetching} className="inline-flex items-center gap-1 rounded px-2 py-1 text-xs text-cyan-700 hover:bg-cyan-50 disabled:opacity-50 dark:text-cyan-300 dark:hover:bg-cyan-400/10"><RefreshCw size={12} className={runtimeUpdates.isFetching ? 'animate-spin' : ''}/>Check now</button>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-neutral-200 p-3 text-sm dark:border-neutral-700">
|
||||||
|
{runtimeUpdates.isLoading ? <p className="text-neutral-500">Checking trusted runtime catalog…</p> : runtimeUpdates.data?.updates.length ? (
|
||||||
|
<div className="space-y-2">{runtimeUpdates.data.updates.map((update) => <div key={update.component} className="flex items-center justify-between"><span className="font-medium uppercase">{update.component}</span><span className="text-xs text-amber-700 dark:text-amber-300">{update.installedVersion} → {update.availableVersion}</span></div>)}</div>
|
||||||
|
) : <p className="flex items-center gap-2 text-neutral-600 dark:text-neutral-300"><CheckCircle2 size={15} className="text-emerald-600"/>Installed Aurora runtimes are current.</p>}
|
||||||
|
{runtimeUpdates.data?.message && <p className="mt-2 text-xs text-neutral-500">Using the {runtimeUpdates.data.source} catalog. {runtimeUpdates.data.message}</p>}
|
||||||
|
<p className="mt-2 text-xs text-neutral-400">Updates are announced automatically. Installation remains a user-approved action.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-2 text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
<h3 className="mb-2 text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
Theme
|
Theme
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
|
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
|
||||||
import type { AuroraRuntimeStatus } from '@shared/types'
|
import type { AuroraRuntimeStatus, AuroraRuntimeUpdateStatus } from '@shared/types'
|
||||||
|
|
||||||
export function useRuntimeStatus(): UseQueryResult<AuroraRuntimeStatus, Error> {
|
export function useRuntimeStatus(): UseQueryResult<AuroraRuntimeStatus, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -8,3 +8,13 @@ export function useRuntimeStatus(): UseQueryResult<AuroraRuntimeStatus, Error> {
|
|||||||
staleTime: 30000
|
staleTime: 30000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRuntimeUpdates(): UseQueryResult<AuroraRuntimeUpdateStatus, Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['runtime', 'updates'],
|
||||||
|
queryFn: () => window.api.runtime.updates(),
|
||||||
|
staleTime: 60 * 60 * 1000,
|
||||||
|
refetchInterval: 6 * 60 * 60 * 1000,
|
||||||
|
retry: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,38 @@ export interface AuroraRuntimeStatus {
|
|||||||
native: { available: boolean; platform: string; arch: string; runtimeVersion?: string; components: AuroraNativeRuntimeComponent[]; reason?: string }
|
native: { available: boolean; platform: string; arch: string; runtimeVersion?: string; components: AuroraNativeRuntimeComponent[]; reason?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuroraRuntimeComponentId = AuroraNativeRuntimeComponent['id']
|
||||||
|
|
||||||
|
export interface AuroraRuntimeCatalogRelease {
|
||||||
|
component: AuroraRuntimeComponentId
|
||||||
|
version: string
|
||||||
|
platform: AuroraNativeRuntimeManifest['platform']
|
||||||
|
arch: AuroraNativeRuntimeManifest['arch']
|
||||||
|
channel: 'stable' | 'security'
|
||||||
|
url: string
|
||||||
|
sha256: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuroraRuntimeCatalog {
|
||||||
|
schema: 1
|
||||||
|
generatedAt: string
|
||||||
|
releases: AuroraRuntimeCatalogRelease[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuroraRuntimeUpdate {
|
||||||
|
component: AuroraRuntimeComponentId
|
||||||
|
installedVersion: string
|
||||||
|
availableVersion: string
|
||||||
|
channel: AuroraRuntimeCatalogRelease['channel']
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuroraRuntimeUpdateStatus {
|
||||||
|
checkedAt: string
|
||||||
|
source: 'bundled' | 'remote' | 'cache'
|
||||||
|
updates: AuroraRuntimeUpdate[]
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuroraProjectSummary {
|
export interface AuroraProjectSummary {
|
||||||
name: string
|
name: string
|
||||||
status: ProjectStatus
|
status: ProjectStatus
|
||||||
|
|||||||
Reference in New Issue
Block a user