238 lines
6.4 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|