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.
@@ -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
- <ManagedAppRow app={app} key={app.id} onJob={(job) => { setJobs((current) => ({ ...current, [job.id]: job })); setSelectedJobId(job.id); }} />
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
- {selectedJob ? <AppActionTerminal job={selectedJob} /> : <p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream here. 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>}
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 ManagedAppRow({ app, onJob }: { app: GuiManagedAppSummary; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
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-row">
51
- <div className="managed-app-main">
52
- <div>
53
- <p className="eyebrow">{app.category}</p>
54
- <h3>{app.name}</h3>
55
- <p>{app.description}</p>
56
- </div>
57
- <div className="managed-app-links">
58
- <OriginLink href={app.origin_website_url} label={text.website} />
59
- <OriginLink href={app.origin_repo_url} label="Repo" />
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
- <div className="managed-app-actions">
71
- {app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} title={action.command_preview} />)}
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, title }: { action: GuiAppActionId; app: GuiManagedAppSummary; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void; title: string }): ReactElement {
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()} title={title} type="button">{icon}</button>;
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
- label: text.management,
393
- mode: "comms",
394
- items: [
395
- plannedNavItem("channels", text.channels, "Team channels", "hash"),
396
- plannedNavItem("directMessages", text.directMessages, "Direct and group DMs", "message"),
397
- { id: "emailAccounts", label: text.emailAccounts, description: "Mail account sync", icon: "mail", badge: text.planned },
398
- { id: "messagingAccounts", label: text.messagingAccounts, description: "Messaging account sync", icon: "message", badge: text.planned },
399
- { id: "events", label: text.calendars, description: "CalDAV calendar sync", icon: "calendar", badge: text.planned },
400
- { id: "contacts", label: text.contacts, description: "CardDAV contact sync", icon: "users", badge: text.planned },
401
- { id: "tasks", label: text.tasks, description: "CalDAV task sync", icon: "list", badge: text.planned },
402
- { id: "notes", label: text.notes, description: "Synced notes", icon: "note", badge: text.planned },
403
- { id: "bookmarks", label: text.bookmarks, description: "Saved links and references", icon: "bookmark", badge: text.planned },
404
- ],
405
- },
406
- {
407
- label: text.projectWork,
408
- mode: "comms",
409
- items: [
410
- { id: "inbox", label: text.inbox, description: "Project intake", icon: "folder", badge: text.planned },
411
- { id: "campaigns", label: text.campaigns, description: "Campaign folders", icon: "folder", badge: text.planned },
412
- { id: "cases", label: text.cases, description: "Case folders", icon: "folder", badge: text.planned },
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,