aidevops 3.29.5 → 3.29.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/status-adapter.ts +13 -1
- package/packages/gui-shared/src/contracts.ts +26 -0
- package/packages/gui-shared/src/fixtures.ts +13 -0
- package/packages/gui-shared/src/index.ts +1 -0
- package/packages/gui-shared/src/notifications.ts +220 -0
- package/packages/gui-web/src/AppWorkspace.tsx +87 -19
- package/packages/gui-web/src/InventorySurfaces.tsx +74 -21
- package/packages/gui-web/src/app-model.ts +27 -30
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +390 -27
- package/setup.sh +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
2
|
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useState } from "react";
|
|
3
|
-
import { FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
3
|
+
import { FiChevronDown, FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
4
4
|
import type { InventoryColumn } from "./app-model";
|
|
5
5
|
import { installationRows, text } from "./app-model";
|
|
6
6
|
|
|
@@ -13,9 +13,17 @@ let draftRowCounter = 0;
|
|
|
13
13
|
|
|
14
14
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
15
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
16
|
+
const [expandedAppIds, setExpandedAppIds] = useState<Set<string>>(() => new Set(status.managed_apps.length > 0 ? [status.managed_apps[0].id] : []));
|
|
16
17
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
17
18
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
18
19
|
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (status.managed_apps.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setExpandedAppIds((current) => current.size === 0 ? new Set([status.managed_apps[0].id]) : current);
|
|
25
|
+
}, [status.managed_apps]);
|
|
26
|
+
|
|
19
27
|
useEffect(() => {
|
|
20
28
|
if (selectedJob === null || selectedJob.status !== "running") {
|
|
21
29
|
return undefined;
|
|
@@ -37,28 +45,51 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
37
45
|
</div>
|
|
38
46
|
<div className="managed-app-list">
|
|
39
47
|
{status.managed_apps.map((app) => (
|
|
40
|
-
<
|
|
48
|
+
<ManagedAppPanel
|
|
49
|
+
app={app}
|
|
50
|
+
expanded={expandedAppIds.has(app.id)}
|
|
51
|
+
job={jobForApp(app.id, jobs, selectedJob)}
|
|
52
|
+
key={app.id}
|
|
53
|
+
onJob={(job) => {
|
|
54
|
+
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
55
|
+
setSelectedJobId(job.id);
|
|
56
|
+
setExpandedAppIds((current) => toggledAppIds(current, app.id, true));
|
|
57
|
+
}}
|
|
58
|
+
onToggle={() => setExpandedAppIds((current) => toggledAppIds(current, app.id))}
|
|
59
|
+
/>
|
|
41
60
|
))}
|
|
42
61
|
</div>
|
|
43
|
-
|
|
62
|
+
<p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
|
|
44
63
|
</section>
|
|
45
64
|
);
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
function
|
|
67
|
+
function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onToggle: () => void }): ReactElement {
|
|
49
68
|
return (
|
|
50
|
-
<article className="managed-app-
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
</
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
<
|
|
69
|
+
<article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
|
|
70
|
+
<button aria-expanded={expanded} className="managed-app-summary" data-tooltip={`${expanded ? "Collapse" : "Expand"} ${app.name} controls`} onClick={onToggle} type="button">
|
|
71
|
+
<span className="managed-app-title-block">
|
|
72
|
+
<span className="eyebrow">{app.category}</span>
|
|
73
|
+
<strong>{app.name}</strong>
|
|
74
|
+
<span>{app.description}</span>
|
|
75
|
+
</span>
|
|
76
|
+
<span className="managed-app-summary-meta">
|
|
77
|
+
<SummaryChip label="Status" value={app.status} />
|
|
78
|
+
<SummaryChip label="Installed" value={app.installed_version} />
|
|
79
|
+
<SummaryChip label="Latest" value={app.latest_version} />
|
|
80
|
+
<FiChevronDown aria-hidden="true" className="managed-app-chevron" />
|
|
81
|
+
</span>
|
|
82
|
+
</button>
|
|
83
|
+
{expanded ? <div className="managed-app-body">
|
|
84
|
+
<div className="managed-app-toolbar">
|
|
85
|
+
<div className="managed-app-links">
|
|
86
|
+
<OriginLink href={app.origin_website_url} label={text.website} />
|
|
87
|
+
<OriginLink href={app.origin_repo_url} label="Repo" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="managed-app-actions">
|
|
90
|
+
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} commandPreview={action.command_preview} />)}
|
|
91
|
+
</div>
|
|
60
92
|
</div>
|
|
61
|
-
</div>
|
|
62
93
|
<div className="managed-app-details">
|
|
63
94
|
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
64
95
|
<ToggleSwitch checked={app.aidevops_update} label="update maintains" />
|
|
@@ -67,19 +98,22 @@ function ManagedAppRow({ app, onJob }: { app: GuiManagedAppSummary; onJob: (job:
|
|
|
67
98
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
68
99
|
<AppMeta label="Status" value={app.status} />
|
|
69
100
|
</div>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
</div>
|
|
101
|
+
{job ? <AppActionTerminal job={job} /> : <p className="empty-state compact-notice">No recent command output for this app. Run an action to open this app's terminal log.</p>}
|
|
102
|
+
</div> : null}
|
|
73
103
|
</article>
|
|
74
104
|
);
|
|
75
105
|
}
|
|
76
106
|
|
|
107
|
+
function SummaryChip({ label, value }: { label: string; value: string }): ReactElement {
|
|
108
|
+
return <span className="managed-summary-chip"><small>{label}</small><span>{value}</span></span>;
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
function OriginLink({ href, label }: { href: string; label: string }): ReactElement {
|
|
78
112
|
if (href.length === 0) {
|
|
79
113
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
return <a href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
116
|
+
return <a data-tooltip={`Open ${label.toLowerCase()} destination`} href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
@@ -90,7 +124,7 @@ function AppMeta({ label, value }: { label: string; value: string }): ReactEleme
|
|
|
90
124
|
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
91
125
|
}
|
|
92
126
|
|
|
93
|
-
function AppActionButton({ action, app, disabled, onJob
|
|
127
|
+
function AppActionButton({ action, app, commandPreview, disabled, onJob }: { action: GuiAppActionId; app: GuiManagedAppSummary; commandPreview: string; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
|
|
94
128
|
const icon = action === "install" ? <FiDownload /> : action === "update" ? <FiRefreshCw /> : action === "reinstall" ? <FiRepeat /> : <FiTrash2 />;
|
|
95
129
|
|
|
96
130
|
async function runAction(): Promise<void> {
|
|
@@ -106,7 +140,7 @@ function AppActionButton({ action, app, disabled, onJob, title }: { action: GuiA
|
|
|
106
140
|
onJob(envelope.data);
|
|
107
141
|
}
|
|
108
142
|
|
|
109
|
-
return <button aria-label={`${action} ${app.name}`} className="app-action-button" disabled={disabled} onClick={() => void runAction()}
|
|
143
|
+
return <button aria-label={`${action} ${app.name}`} className={action === "remove" ? "app-action-button remove" : "app-action-button"} data-tooltip={commandPreview} disabled={disabled} onClick={() => void runAction()} type="button">{icon}<span>{action}</span></button>;
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
@@ -127,6 +161,25 @@ async function refreshJob(jobId: string, setJobs: Dispatch<SetStateAction<Record
|
|
|
127
161
|
setJobs((current) => ({ ...current, [envelope.data.id]: envelope.data }));
|
|
128
162
|
}
|
|
129
163
|
|
|
164
|
+
function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>, selectedJob: GuiAppActionJobSummary | null): GuiAppActionJobSummary | null {
|
|
165
|
+
if (selectedJob?.app_id === appId) {
|
|
166
|
+
return selectedJob;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Object.values(jobs).reverse().find((job) => job.app_id === appId) ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toggledAppIds(current: Set<string>, appId: string, forceOpen = false): Set<string> {
|
|
173
|
+
const next = new Set(current);
|
|
174
|
+
if (forceOpen || !next.has(appId)) {
|
|
175
|
+
next.add(appId);
|
|
176
|
+
} else {
|
|
177
|
+
next.delete(appId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
|
|
130
183
|
export function InstallationSurface(): ReactElement {
|
|
131
184
|
return (
|
|
132
185
|
<section className="panel" aria-label={text.installation}>
|
|
@@ -330,6 +330,10 @@ function plannedNavItem(id: SurfaceId, label: string, description: string, icon:
|
|
|
330
330
|
return { id, label, description, icon, badge: text.planned };
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
function plannedCommsGroup(label: string, items: SurfaceNavItem[]): SurfaceNavGroup {
|
|
334
|
+
return { label, mode: "comms", items };
|
|
335
|
+
}
|
|
336
|
+
|
|
333
337
|
export const navGroups: SurfaceNavGroup[] = [
|
|
334
338
|
{
|
|
335
339
|
label: text.development,
|
|
@@ -388,36 +392,28 @@ export const navGroups: SurfaceNavGroup[] = [
|
|
|
388
392
|
{ id: "marketplaces", label: text.marketplaces, description: "Storefronts and seller profiles", icon: "package", badge: text.planned },
|
|
389
393
|
],
|
|
390
394
|
},
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
{ id: "projectConfig", label: text.config, description: "Project configuration", icon: "folder", badge: text.planned },
|
|
414
|
-
{ id: "feedback", label: text.feedback, description: "Feedback folders", icon: "folder", badge: text.planned },
|
|
415
|
-
{ id: "knowledge", label: text.knowledge, description: "Knowledge folders", icon: "folder", badge: text.planned },
|
|
416
|
-
{ id: "maintenance", label: text.maintenance, description: "Maintenance folders", icon: "folder", badge: text.planned },
|
|
417
|
-
{ id: "performance", label: text.performance, description: "Performance folders", icon: "folder", badge: text.planned },
|
|
418
|
-
{ id: "reports", label: text.reports, description: "Report folders", icon: "folder", badge: text.planned },
|
|
419
|
-
],
|
|
420
|
-
},
|
|
395
|
+
plannedCommsGroup(text.management, [
|
|
396
|
+
plannedNavItem("channels", text.channels, "Team channels", "hash"),
|
|
397
|
+
plannedNavItem("directMessages", text.directMessages, "Direct and group DMs", "message"),
|
|
398
|
+
plannedNavItem("emailAccounts", text.emailAccounts, "Mail account sync", "mail"),
|
|
399
|
+
plannedNavItem("messagingAccounts", text.messagingAccounts, "Messaging account sync", "message"),
|
|
400
|
+
plannedNavItem("events", text.calendars, "CalDAV calendar sync", "calendar"),
|
|
401
|
+
plannedNavItem("contacts", text.contacts, "CardDAV contact sync", "users"),
|
|
402
|
+
plannedNavItem("tasks", text.tasks, "CalDAV task sync", "list"),
|
|
403
|
+
plannedNavItem("notes", text.notes, "Synced notes", "note"),
|
|
404
|
+
plannedNavItem("bookmarks", text.bookmarks, "Saved links and references", "bookmark"),
|
|
405
|
+
]),
|
|
406
|
+
plannedCommsGroup(text.projectWork, [
|
|
407
|
+
plannedNavItem("inbox", text.inbox, "Project intake", "folder"),
|
|
408
|
+
plannedNavItem("campaigns", text.campaigns, "Campaign folders", "folder"),
|
|
409
|
+
plannedNavItem("cases", text.cases, "Case folders", "folder"),
|
|
410
|
+
plannedNavItem("projectConfig", text.config, "Project configuration", "folder"),
|
|
411
|
+
plannedNavItem("feedback", text.feedback, "Feedback folders", "folder"),
|
|
412
|
+
plannedNavItem("knowledge", text.knowledge, "Knowledge folders", "folder"),
|
|
413
|
+
plannedNavItem("maintenance", text.maintenance, "Maintenance folders", "folder"),
|
|
414
|
+
plannedNavItem("performance", text.performance, "Performance folders", "folder"),
|
|
415
|
+
plannedNavItem("reports", text.reports, "Report folders", "folder"),
|
|
416
|
+
]),
|
|
421
417
|
];
|
|
422
418
|
|
|
423
419
|
export const dashboardNavItem: SurfaceNavItem = {
|
|
@@ -607,6 +603,7 @@ export function surfaceRecordCounts(status: GuiStatusData): SurfaceRecordCounts
|
|
|
607
603
|
hosts: populatedInventoryRowCount("hosts"),
|
|
608
604
|
installation: installationRows.length,
|
|
609
605
|
localSetup: status.paths.length,
|
|
606
|
+
notifications: status.notifications.filter((notification) => notification.status === "active").length,
|
|
610
607
|
projects: status.repos.total || status.repos.repos.length,
|
|
611
608
|
repos: (status.local_repos.total || status.local_repos.repos.length) + (status.repos.total || status.repos.repos.length),
|
|
612
609
|
registrars: populatedInventoryRowCount("registrars"),
|
|
@@ -55,7 +55,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
|
|
|
55
55
|
`<h1>aidevops app interface</h1>`,
|
|
56
56
|
`<p>Made for creators.</p>`,
|
|
57
57
|
`<p>AI-assisted development workflows, code quality, and deployment automation.</p>`,
|
|
58
|
-
`<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Menlo (default), Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. The workspace header includes a command palette, reserved signposts/help slot immediately left of notifications, notifications menu, AI Assistant panel, and profile menu with Settings, Theme, Language, Community, Admin, and Logout entries. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
|
|
58
|
+
`<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Menlo (default), Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. The workspace header includes a command palette, reserved signposts/help slot immediately left of notifications, notifications menu, AI Assistant panel, and profile menu with Settings, Theme, Language, Community, Admin, and Logout entries. Notifications reuse the OpenCode toast status cache and GUI readiness data. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
|
|
59
59
|
`<p>Secret references stay read-only and redacted.</p>`,
|
|
60
60
|
`<p>Vault surfaces show padlock indicators. Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.</p>`,
|
|
61
61
|
`<p>${escapeHtml(status.data.update.message)}</p>`,
|
|
@@ -87,6 +87,7 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
|
|
|
87
87
|
setup_targets: data.setup_targets ?? statusFixture.setup_targets,
|
|
88
88
|
ai_apps: data.ai_apps ?? statusFixture.ai_apps,
|
|
89
89
|
managed_apps: data.managed_apps ?? statusFixture.managed_apps,
|
|
90
|
+
notifications: data.notifications ?? statusFixture.notifications,
|
|
90
91
|
vault: {
|
|
91
92
|
...statusFixture.vault,
|
|
92
93
|
...data.vault,
|