feat(ecoos-daemon): integrate a bundled daemon web UI with components, interfaces, styles, bundling config, and server support
This commit is contained in:
185
ecoos_daemon/ts_web/elements/ecoos-updates.ts
Normal file
185
ecoos_daemon/ts_web/elements/ecoos-updates.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user