Files
eco_os/ecoos_daemon/ts_web/elements/ecoos-updates.ts
2026-01-12 14:34:56 +00:00

238 lines
6.4 KiB
TypeScript

/**
* 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`<div class="empty">Loading...</div>`;
}
const newerReleases = this.updateInfo.releases.filter(r => r.isNewer);
const { autoUpgrade, lastCheck } = this.updateInfo;
return html`
<div class="page">
<!-- Current Version -->
<dees-panel .title=${'Current Version'}>
<div class="version-display">v${this.updateInfo.currentVersion}</div>
${lastCheck ? html`<div class="last-check">Last check: ${new Date(lastCheck).toLocaleString()}</div>` : ''}
<dees-button
.type=${'default'}
.text=${this.loading ? 'Checking...' : 'Check for Updates'}
.disabled=${this.loading}
@click=${this.checkForUpdates}
></dees-button>
</dees-panel>
<!-- Auto-upgrade Banner -->
${autoUpgrade?.targetVersion ? html`
<dees-panel .variant=${'outline'} class="banner-upgrade">
<div class="banner-content">
${autoUpgrade.waitingForStability
? html`Auto-upgrade to <strong>v${autoUpgrade.targetVersion}</strong> in ${autoUpgrade.scheduledIn}`
: html`Auto-upgrade to <strong>v${autoUpgrade.targetVersion}</strong> pending`
}
</div>
</dees-panel>
` : ''}
<!-- Available Updates -->
<dees-panel .title=${'Available Updates'}>
${newerReleases.length === 0
? html`<div class="empty-text">You're up to date</div>`
: newerReleases.map(r => html`
<div class="update-row">
<span class="update-version">v${r.version}</span>
<span class="update-age">${formatAge(r.ageHours)}</span>
<dees-button
.type=${'default'}
.status=${'success'}
.text=${'Upgrade'}
.disabled=${this.loading}
@click=${() => this.upgradeToVersion(r.version)}
></dees-button>
</div>
`)
}
</dees-panel>
<!-- Message -->
${this.message ? html`
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
` : ''}
</div>
`;
}
private async checkForUpdates(): Promise<void> {
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<void> {
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;
}
}
}