Files
eco_os/ecoos_daemon/ts_web/elements/ecoos-updates.ts

186 lines
4.6 KiB
TypeScript

/**
* EcoOS Updates View
* Version info, available updates, and upgrade controls
*/
import {
html,
DeesElement,
customElement,
property,
state,
css,
type TemplateResult,
} from '@design.estate/dees-element';
import { sharedStyles, formatAge } from '../styles/shared.js';
import type { IUpdateInfo, IRelease } 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;
public static styles = [
sharedStyles,
css`
:host {
display: block;
padding: 20px;
}
.release-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--ecoos-border);
}
.release-item:last-child {
border-bottom: none;
}
.release-version {
font-weight: 500;
}
.release-age {
color: var(--ecoos-text-dim);
font-size: 12px;
}
.release-item .btn {
padding: 4px 12px;
margin: 0;
font-size: 11px;
}
.auto-upgrade-status {
margin-top: 16px;
font-size: 12px;
color: var(--ecoos-text-dim);
}
.actions {
margin-top: 16px;
}
`,
];
render(): TemplateResult {
if (!this.updateInfo) {
return html`<div>Loading...</div>`;
}
const newerReleases = this.updateInfo.releases.filter(r => r.isNewer);
return html`
<div class="card">
<div class="card-title">Updates</div>
<div class="stat">
<div class="stat-label">Current Version</div>
<div class="stat-value">v${this.updateInfo.currentVersion}</div>
</div>
<div style="margin: 16px 0;">
${newerReleases.length === 0
? html`<div style="color: var(--ecoos-text-dim)">No updates available</div>`
: newerReleases.map(r => this.renderRelease(r))
}
</div>
<div class="auto-upgrade-status">
${this.renderAutoUpgradeStatus()}
</div>
<div class="actions">
<button
class="btn btn-primary"
@click=${this.checkForUpdates}
?disabled=${this.loading}
>
Check for Updates
</button>
</div>
</div>
`;
}
private renderRelease(release: IRelease): TemplateResult {
return html`
<div class="release-item">
<span>
<span class="release-version">v${release.version}</span>
<span class="release-age">(${formatAge(release.ageHours)})</span>
</span>
<button
class="btn btn-primary"
@click=${() => this.upgradeToVersion(release.version)}
?disabled=${this.loading}
>
Upgrade
</button>
</div>
`;
}
private renderAutoUpgradeStatus(): TemplateResult {
const { autoUpgrade, lastCheck } = this.updateInfo!;
if (autoUpgrade.targetVersion) {
if (autoUpgrade.waitingForStability) {
return html`Auto-upgrade to v${autoUpgrade.targetVersion} in ${autoUpgrade.scheduledIn} (stability period)`;
}
return html`Auto-upgrade to v${autoUpgrade.targetVersion} pending...`;
}
if (lastCheck) {
return html`Last checked: ${new Date(lastCheck).toLocaleTimeString()}`;
}
return html``;
}
private async checkForUpdates(): Promise<void> {
this.loading = true;
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);
} finally {
this.loading = false;
}
}
private async upgradeToVersion(version: string): Promise<void> {
if (!confirm(`Upgrade to version ${version}? The daemon will restart.`)) return;
this.loading = true;
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.dispatchEvent(new CustomEvent('upgrade-started', { detail: result }));
} else {
alert(`Upgrade failed: ${result.message}`);
}
} catch (error) {
alert(`Upgrade error: ${error}`);
} finally {
this.loading = false;
}
}
}