/** * EcoOS Updates View * Card-based update management */ import { html, DeesElement, customElement, property, state, css, type TemplateResult, } from '@design.estate/dees-element'; import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog'; import { sharedStyles, formatAge } from '../styles/shared.js'; import type { IUpdateInfo } from '../../ts_interfaces/updates.js'; @customElement('ecoos-updates') export class EcoosUpdates extends DeesElement { @property({ type: Object }) public accessor updateInfo: IUpdateInfo | null = null; @state() private accessor loading: boolean = false; @state() private accessor message: string = ''; @state() private accessor messageError: boolean = false; public static styles = [ sharedStyles, css` :host { display: block; padding: 16px; } .page { display: flex; flex-direction: column; gap: 16px; } .version-display { font-size: var(--text-2xl); font-weight: 600; font-family: 'SF Mono', monospace; margin-bottom: 8px; } .last-check { font-size: var(--text-xs); color: var(--text-tertiary); margin-bottom: 12px; } .banner-upgrade { background: hsla(217.2, 91.2%, 59.8%, 0.1); border-color: hsla(217.2, 91.2%, 59.8%, 0.3); } .banner-content { display: flex; align-items: center; gap: 8px; font-size: var(--text-sm); } .banner-content strong { font-family: 'SF Mono', monospace; } .update-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); gap: 12px; } .update-row:last-child { border-bottom: none; padding-bottom: 0; } .update-row:first-child { padding-top: 0; } .update-version { font-family: 'SF Mono', monospace; font-size: var(--text-sm); font-weight: 500; } .update-age { font-size: var(--text-xs); color: var(--text-tertiary); flex: 1; } .empty-text { font-size: var(--text-sm); color: var(--text-tertiary); padding: 8px 0; } .message-bar { padding: 8px 12px; border-radius: 6px; font-size: var(--text-sm); } .message-bar.success { background: hsla(142.1, 76.2%, 36.3%, 0.15); color: var(--success); } .message-bar.error { background: hsla(0, 84.2%, 60.2%, 0.15); color: var(--error); } `, ]; render(): TemplateResult { if (!this.updateInfo) { return html`
Loading...
`; } const newerReleases = this.updateInfo.releases.filter(r => r.isNewer); const { autoUpgrade, lastCheck } = this.updateInfo; return html`
v${this.updateInfo.currentVersion}
${lastCheck ? html`
Last check: ${new Date(lastCheck).toLocaleString()}
` : ''}
${autoUpgrade?.targetVersion ? html` ` : ''} ${newerReleases.length === 0 ? html`
You're up to date
` : newerReleases.map(r => html`
v${r.version} ${formatAge(r.ageHours)} this.upgradeToVersion(r.version)} >
`) }
${this.message ? html`
${this.message}
` : ''}
`; } private async checkForUpdates(): Promise { this.loading = true; this.message = ''; try { const response = await fetch('/api/updates/check', { method: 'POST' }); const result = await response.json(); this.updateInfo = result; this.dispatchEvent(new CustomEvent('updates-checked', { detail: result })); } catch (error) { console.error('Failed to check updates:', error); this.message = `Failed: ${error}`; this.messageError = true; } finally { this.loading = false; } } private async upgradeToVersion(version: string): Promise { if (!confirm(`Upgrade to v${version}?`)) return; this.loading = true; this.message = ''; try { const response = await fetch('/api/upgrade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version }), }); const result = await response.json(); if (result.success) { this.message = result.message; this.messageError = false; this.dispatchEvent(new CustomEvent('upgrade-started', { detail: result })); } else { this.message = `Failed: ${result.message}`; this.messageError = true; } } catch (error) { this.message = `Error: ${error}`; this.messageError = true; } finally { this.loading = false; } } }