aidevops 3.29.5 → 3.29.6
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/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 +178 -0
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.29.
|
|
1
|
+
3.29.6
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type GuiSetupTargetSummary,
|
|
21
21
|
type GuiStatusData,
|
|
22
22
|
type GuiVaultStatusData,
|
|
23
|
+
buildStatusNotifications,
|
|
23
24
|
statusFixture,
|
|
24
25
|
VAULT_STATUS_ROUTE_MANIFEST,
|
|
25
26
|
} from "../../gui-shared/src";
|
|
@@ -60,6 +61,7 @@ export function readStatus(
|
|
|
60
61
|
const reposPathRef = "~/.config/aidevops/repos.json";
|
|
61
62
|
const oauthPoolPathRef = "~/.aidevops/oauth-pool.json";
|
|
62
63
|
const opencodeDbPathRef = "~/.local/share/opencode/opencode.db";
|
|
64
|
+
const greetingCachePathRef = "~/.aidevops/cache/session-greeting.txt";
|
|
63
65
|
const agentsPathRef = "~/.aidevops/agents";
|
|
64
66
|
const vaultPathRef = statusFixture.vault.path_ref;
|
|
65
67
|
const settingsPath = expandHome(settingsPathRef);
|
|
@@ -76,13 +78,22 @@ export function readStatus(
|
|
|
76
78
|
const managedApps = readManagedApps(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
77
79
|
const localRepos = readLocalReposSetupSummary(reposPath);
|
|
78
80
|
const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
|
|
81
|
+
const oauthPool = readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef);
|
|
79
82
|
const vault = readVaultSummary(repoRoot);
|
|
83
|
+
const notifications = buildStatusNotifications({
|
|
84
|
+
aiApps,
|
|
85
|
+
greetingOutput: readOptionalText(expandHome(greetingCachePathRef)) ?? "",
|
|
86
|
+
oauthPool,
|
|
87
|
+
restartRequired,
|
|
88
|
+
setupTargets,
|
|
89
|
+
});
|
|
80
90
|
const sourcePathRefs = [
|
|
81
91
|
agentsPathRef,
|
|
82
92
|
settingsPathRef,
|
|
83
93
|
reposPathRef,
|
|
84
94
|
oauthPoolPathRef,
|
|
85
95
|
opencodeDbPathRef,
|
|
96
|
+
greetingCachePathRef,
|
|
86
97
|
vaultPathRef,
|
|
87
98
|
"VERSION",
|
|
88
99
|
...setupTargets.map((target) => target.path_ref),
|
|
@@ -131,10 +142,11 @@ export function readStatus(
|
|
|
131
142
|
repos: readRepoRegistrySummary(reposPath, reposPathRef),
|
|
132
143
|
local_repos: localRepos,
|
|
133
144
|
opencode_sessions: opencodeSessions,
|
|
134
|
-
oauth_pool:
|
|
145
|
+
oauth_pool: oauthPool,
|
|
135
146
|
setup_targets: setupTargets,
|
|
136
147
|
ai_apps: aiApps,
|
|
137
148
|
managed_apps: managedApps,
|
|
149
|
+
notifications,
|
|
138
150
|
vault,
|
|
139
151
|
};
|
|
140
152
|
|
|
@@ -339,6 +339,31 @@ export interface GuiCapabilitySummary {
|
|
|
339
339
|
doc_ref: string;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
export type GuiNotificationSeverity = "success" | "info" | "warning" | "error";
|
|
343
|
+
|
|
344
|
+
export type GuiNotificationCategory = "security" | "maintenance" | "release" | "runtime" | "setup";
|
|
345
|
+
|
|
346
|
+
export interface GuiNotificationAction {
|
|
347
|
+
id: string;
|
|
348
|
+
label: string;
|
|
349
|
+
kind: "surface" | "command";
|
|
350
|
+
surface_id?: string;
|
|
351
|
+
command_preview?: string;
|
|
352
|
+
enabled: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface GuiNotificationSummary {
|
|
356
|
+
id: string;
|
|
357
|
+
title: string;
|
|
358
|
+
message: string;
|
|
359
|
+
severity: GuiNotificationSeverity;
|
|
360
|
+
category: GuiNotificationCategory;
|
|
361
|
+
source: "opencode-toast" | "gui-status";
|
|
362
|
+
source_ref: string;
|
|
363
|
+
status: "active" | "resolved";
|
|
364
|
+
actions: GuiNotificationAction[];
|
|
365
|
+
}
|
|
366
|
+
|
|
342
367
|
export interface GuiMachineSummary {
|
|
343
368
|
id: string;
|
|
344
369
|
label: string;
|
|
@@ -451,6 +476,7 @@ export interface GuiStatusData {
|
|
|
451
476
|
setup_targets: GuiSetupTargetSummary[];
|
|
452
477
|
ai_apps: GuiAiAppSummary[];
|
|
453
478
|
managed_apps: GuiManagedAppSummary[];
|
|
479
|
+
notifications: GuiNotificationSummary[];
|
|
454
480
|
vault: GuiVaultStatusData;
|
|
455
481
|
capabilities: GuiCapabilitySummary[];
|
|
456
482
|
secrets: GuiSecretReference[];
|
|
@@ -163,6 +163,19 @@ export const statusFixture: GuiStatusData = {
|
|
|
163
163
|
},
|
|
164
164
|
],
|
|
165
165
|
managed_apps: [],
|
|
166
|
+
notifications: [
|
|
167
|
+
{
|
|
168
|
+
id: "fixture-security-active",
|
|
169
|
+
title: "Security protections active",
|
|
170
|
+
message: "Security: all protections active",
|
|
171
|
+
severity: "success",
|
|
172
|
+
category: "security",
|
|
173
|
+
source: "opencode-toast",
|
|
174
|
+
source_ref: "~/.aidevops/cache/session-greeting.txt",
|
|
175
|
+
status: "resolved",
|
|
176
|
+
actions: [{ id: "open-security", label: "Open security", kind: "surface", surface_id: "security", enabled: true }],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
166
179
|
vault: {
|
|
167
180
|
status: "uninitialized",
|
|
168
181
|
setup_state: "uninitialized",
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { GuiAiAppSummary, GuiNotificationAction, GuiNotificationCategory, GuiNotificationSeverity, GuiNotificationSummary, GuiOAuthPoolSummary, GuiSetupTargetSummary } from "./contracts";
|
|
2
|
+
|
|
3
|
+
interface BuildNotificationInput {
|
|
4
|
+
aiApps: GuiAiAppSummary[];
|
|
5
|
+
greetingOutput: string;
|
|
6
|
+
oauthPool: GuiOAuthPoolSummary;
|
|
7
|
+
restartRequired: boolean;
|
|
8
|
+
setupTargets: GuiSetupTargetSummary[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const toastSeverityRank: Record<GuiNotificationSeverity, number> = {
|
|
12
|
+
success: 0,
|
|
13
|
+
info: 1,
|
|
14
|
+
warning: 2,
|
|
15
|
+
error: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const warningLineMatchers: Array<(line: string) => boolean> = [
|
|
19
|
+
(line) => line.startsWith("Pulse stalled"),
|
|
20
|
+
(line) => /contribution\(s\) need/i.test(line),
|
|
21
|
+
(line) => line.startsWith("[OPENCODE MAINTENANCE]"),
|
|
22
|
+
(line) => line.startsWith("[WARNING]"),
|
|
23
|
+
(line) => line.startsWith("[WARN]"),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const toastTitleRules: Array<{ matches: (line: string) => boolean; title: string }> = [
|
|
27
|
+
{ matches: (line) => line.startsWith("[SECURITY ADVISORY]"), title: "Security advisory" },
|
|
28
|
+
{ matches: (line) => line.startsWith("[ERROR]"), title: "Startup error" },
|
|
29
|
+
{ matches: (line) => line.startsWith("[OPENCODE MAINTENANCE]"), title: "OpenCode maintenance" },
|
|
30
|
+
{ matches: (line) => line.startsWith("Pulse stalled"), title: "Pulse stalled" },
|
|
31
|
+
{ matches: (line) => /contribution\(s\) need/i.test(line), title: "External contribution review" },
|
|
32
|
+
{ matches: (line) => line.startsWith("[WARNING]") || line.startsWith("[WARN]"), title: "Startup warning" },
|
|
33
|
+
{ matches: (line) => line.startsWith("Security: all protections active"), title: "Security protections active" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function classifyToastLine(line: string): GuiNotificationSeverity | null {
|
|
37
|
+
const normalized = line.trim();
|
|
38
|
+
if (normalized.length === 0 || normalized.startsWith("UPDATE_AVAILABLE|") || normalized === "AUTO_UPDATE_ENABLED") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (normalized.startsWith("[SECURITY ADVISORY]") || normalized.startsWith("[ERROR]")) {
|
|
43
|
+
return "error";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (warningLineMatchers.some((matches) => matches(normalized))) {
|
|
47
|
+
return "warning";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (normalized.startsWith("Security: all protections active")) {
|
|
51
|
+
return "success";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "info";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildStatusNotifications(input: BuildNotificationInput): GuiNotificationSummary[] {
|
|
58
|
+
const notifications = [
|
|
59
|
+
...buildToastNotifications(input.greetingOutput),
|
|
60
|
+
...buildGuiStateNotifications(input),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return dedupeNotifications(notifications).sort((left, right) => {
|
|
64
|
+
const severityDelta = toastSeverityRank[right.severity] - toastSeverityRank[left.severity];
|
|
65
|
+
return severityDelta === 0 ? left.title.localeCompare(right.title) : severityDelta;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildToastNotifications(greetingOutput: string): GuiNotificationSummary[] {
|
|
70
|
+
return greetingOutput
|
|
71
|
+
.split("\n")
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.flatMap((line) => {
|
|
75
|
+
const severity = classifyToastLine(line);
|
|
76
|
+
if (severity === null) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const category = categoryForToastLine(line, severity);
|
|
81
|
+
return [notification({
|
|
82
|
+
id: `toast-${stableId(line)}`,
|
|
83
|
+
title: titleForToastLine(line, severity),
|
|
84
|
+
message: line,
|
|
85
|
+
severity,
|
|
86
|
+
category,
|
|
87
|
+
source: "opencode-toast",
|
|
88
|
+
source_ref: "~/.aidevops/cache/session-greeting.txt",
|
|
89
|
+
status: severity === "success" || severity === "info" ? "resolved" : "active",
|
|
90
|
+
actions: actionsForToastLine(line, category, severity),
|
|
91
|
+
})];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificationSummary[] {
|
|
96
|
+
const setupNeedsUpdate = input.setupTargets.filter((target) => target.needs_update);
|
|
97
|
+
const appNeedsUpdate = input.aiApps.filter((app) => app.needs_update);
|
|
98
|
+
const authErrors = input.oauthPool.providers.filter((provider) => provider.auth_errors > 0);
|
|
99
|
+
const rateLimited = input.oauthPool.providers.filter((provider) => provider.rate_limited > 0);
|
|
100
|
+
const notifications: GuiNotificationSummary[] = [];
|
|
101
|
+
|
|
102
|
+
if (input.restartRequired) {
|
|
103
|
+
notifications.push(notification({
|
|
104
|
+
id: "gui-restart-required",
|
|
105
|
+
title: "Restart required",
|
|
106
|
+
message: "aidevops has updated in the background. Restart the GUI app to use the latest installed version.",
|
|
107
|
+
severity: "warning",
|
|
108
|
+
category: "release",
|
|
109
|
+
source: "gui-status",
|
|
110
|
+
source_ref: "VERSION and ~/.aidevops/agents/VERSION",
|
|
111
|
+
status: "active",
|
|
112
|
+
actions: [surfaceAction("open-installation", "Open installation", "installation"), commandAction("restart-gui", "Restart GUI app", "Quit and reopen the aidevops GUI app")],
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (setupNeedsUpdate.length > 0 || appNeedsUpdate.length > 0) {
|
|
117
|
+
notifications.push(notification({
|
|
118
|
+
id: "gui-targets-need-update",
|
|
119
|
+
title: "Installed targets need update",
|
|
120
|
+
message: `${setupNeedsUpdate.length} setup target(s) and ${appNeedsUpdate.length} AI app target(s) need the current aidevops files.`,
|
|
121
|
+
severity: "warning",
|
|
122
|
+
category: "setup",
|
|
123
|
+
source: "gui-status",
|
|
124
|
+
source_ref: "setup_targets and ai_apps",
|
|
125
|
+
status: "active",
|
|
126
|
+
actions: [surfaceAction("open-installation", "Open installation", "installation"), commandAction("run-update", "Run update", "aidevops update")],
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const provider of authErrors) {
|
|
131
|
+
notifications.push(notification({
|
|
132
|
+
id: `gui-provider-auth-${provider.provider}`,
|
|
133
|
+
title: `${provider.provider} auth attention`,
|
|
134
|
+
message: `${provider.auth_errors} ${provider.provider} account(s) report auth errors. Token values remain hidden from the GUI payload.`,
|
|
135
|
+
severity: "error",
|
|
136
|
+
category: "runtime",
|
|
137
|
+
source: "gui-status",
|
|
138
|
+
source_ref: input.oauthPool.path_ref,
|
|
139
|
+
status: "active",
|
|
140
|
+
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders"), commandAction("check-oauth-pool", "Check provider pool", `aidevops oauth-pool check ${provider.provider}`)],
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const provider of rateLimited) {
|
|
145
|
+
notifications.push(notification({
|
|
146
|
+
id: `gui-provider-rate-limit-${provider.provider}`,
|
|
147
|
+
title: `${provider.provider} rate limit`,
|
|
148
|
+
message: `${provider.rate_limited} ${provider.provider} account(s) are cooling down. The pool should recover automatically when cooldowns expire.`,
|
|
149
|
+
severity: "warning",
|
|
150
|
+
category: "runtime",
|
|
151
|
+
source: "gui-status",
|
|
152
|
+
source_ref: input.oauthPool.path_ref,
|
|
153
|
+
status: "active",
|
|
154
|
+
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders")],
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return notifications;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function notification(input: GuiNotificationSummary): GuiNotificationSummary {
|
|
162
|
+
return input;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function titleForToastLine(line: string, severity: GuiNotificationSeverity): string {
|
|
166
|
+
const rule = toastTitleRules.find((candidate) => candidate.matches(line));
|
|
167
|
+
return rule?.title ?? (severity === "info" ? "Runtime status" : "aidevops status");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function categoryForToastLine(line: string, severity: GuiNotificationSeverity): GuiNotificationCategory {
|
|
171
|
+
if (line.startsWith("[SECURITY ADVISORY]") || line.startsWith("Security:")) return "security";
|
|
172
|
+
if (line.startsWith("[OPENCODE MAINTENANCE]") || line.startsWith("Pulse stalled")) return "maintenance";
|
|
173
|
+
if (/update|version|running in/i.test(line)) return "release";
|
|
174
|
+
if (severity === "warning") return "setup";
|
|
175
|
+
return "runtime";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function actionsForToastLine(line: string, category: GuiNotificationCategory, severity: GuiNotificationSeverity): GuiNotificationAction[] {
|
|
179
|
+
if (category === "security") {
|
|
180
|
+
return [surfaceAction("open-security", "Open security", "security"), surfaceAction("open-vault", "Open Vault", "vault")];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (line.startsWith("[OPENCODE MAINTENANCE]")) {
|
|
184
|
+
return [surfaceAction("open-local-setup", "Open local setup", "localSetup"), commandAction("run-maintenance", "Run maintenance", "opencode-db-maintenance-helper.sh run")];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (line.startsWith("Pulse stalled")) {
|
|
188
|
+
return [surfaceAction("open-maintenance", "Open maintenance", "maintenance")];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (severity === "warning") {
|
|
192
|
+
return [surfaceAction("open-installation", "Open installation", "installation"), commandAction("run-update", "Run update", "aidevops update")];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return [surfaceAction("open-overview", "Open overview", "overview")];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function surfaceAction(id: string, label: string, surfaceId: string): GuiNotificationAction {
|
|
199
|
+
return { id, label, kind: "surface", surface_id: surfaceId, enabled: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function commandAction(id: string, label: string, commandPreview: string): GuiNotificationAction {
|
|
203
|
+
return { id, label, kind: "command", command_preview: commandPreview, enabled: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableId(value: string): string {
|
|
207
|
+
let hash = 0;
|
|
208
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
209
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0;
|
|
210
|
+
}
|
|
211
|
+
return Math.abs(hash).toString(36);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function dedupeNotifications(notifications: GuiNotificationSummary[]): GuiNotificationSummary[] {
|
|
215
|
+
const byId = new Map<string, GuiNotificationSummary>();
|
|
216
|
+
for (const item of notifications) {
|
|
217
|
+
byId.set(item.id, item);
|
|
218
|
+
}
|
|
219
|
+
return [...byId.values()];
|
|
220
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { type ReactElement, type ReactNode, useEffect, useState } from "react";
|
|
3
|
-
import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiUser } from "react-icons/fi";
|
|
4
|
-
import type { GuiFileRootId, GuiStatusData } from "../../gui-shared/src";
|
|
3
|
+
import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiInfo, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUser } from "react-icons/fi";
|
|
4
|
+
import type { GuiFileRootId, GuiNotificationSummary, GuiStatusData } from "../../gui-shared/src";
|
|
5
5
|
import { SurfaceGlyph } from "./AppNavigation";
|
|
6
6
|
import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
|
|
7
|
-
import { inventorySurfaceConfigs, text } from "./app-model";
|
|
7
|
+
import { inventorySurfaceConfigs, surfaceIds, text } from "./app-model";
|
|
8
8
|
import { CommandPalette, type CommandPaletteSelection, commandPaletteShortcutQuery } from "./CommandPalette";
|
|
9
9
|
import { FileExplorerSurface } from "./FileExplorerSurface";
|
|
10
10
|
import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
|
|
@@ -42,7 +42,7 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
|
|
|
42
42
|
<div className="workspace-scroll">
|
|
43
43
|
{shellMode === "sessions"
|
|
44
44
|
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSessionId} status={status} />
|
|
45
|
-
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />}
|
|
45
|
+
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} openSurface={setActiveSurface} status={status} />}
|
|
46
46
|
</div>
|
|
47
47
|
</section>
|
|
48
48
|
);
|
|
@@ -70,6 +70,7 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
70
70
|
const [profileOpen, setProfileOpen] = useState(false);
|
|
71
71
|
const userInitials = status.machine.initials || "AI";
|
|
72
72
|
const userName = status.machine.username || "Local user";
|
|
73
|
+
const activeNotifications = status.notifications.filter((notification) => notification.status === "active");
|
|
73
74
|
|
|
74
75
|
useEffect(() => {
|
|
75
76
|
const openCommandPalette = (event: KeyboardEvent) => {
|
|
@@ -134,9 +135,9 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
134
135
|
</button>
|
|
135
136
|
<button aria-expanded={notificationsOpen} aria-label="Open notifications" className="header-icon-button" onClick={() => { setNotificationsOpen((current) => !current); setProfileOpen(false); }} type="button">
|
|
136
137
|
<FiBell aria-hidden="true" />
|
|
137
|
-
<span className="notification-dot" aria-hidden="true" />
|
|
138
|
+
{activeNotifications.length > 0 ? <span className="notification-dot" aria-hidden="true" /> : null}
|
|
138
139
|
</button>
|
|
139
|
-
{notificationsOpen ? <NotificationsMenu openSurface={(surface) => openItem({ surface })} /> : null}
|
|
140
|
+
{notificationsOpen ? <NotificationsMenu notifications={status.notifications} openSurface={(surface) => openItem({ surface })} /> : null}
|
|
140
141
|
</div>
|
|
141
142
|
<button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} title="AI sessions (_)" type="button">
|
|
142
143
|
<FiTerminal aria-hidden="true" />
|
|
@@ -176,16 +177,35 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
|
|
|
176
177
|
return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
180
|
+
function NotificationsMenu({ notifications, openSurface }: { notifications: GuiNotificationSummary[]; openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
181
|
+
const active = notifications.filter((notification) => notification.status === "active");
|
|
182
|
+
const preview = notifications.slice(0, 4);
|
|
183
|
+
|
|
180
184
|
return (
|
|
181
185
|
<div className="popover-menu notifications-menu" role="menu">
|
|
182
|
-
<
|
|
183
|
-
|
|
186
|
+
<div className="notifications-menu-heading">
|
|
187
|
+
<strong>Notifications</strong>
|
|
188
|
+
<span>{active.length} active</span>
|
|
189
|
+
</div>
|
|
190
|
+
{preview.length === 0 ? <p>No current aidevops notifications.</p> : null}
|
|
191
|
+
{preview.map((notification) => <NotificationPreview key={notification.id} notification={notification} />)}
|
|
184
192
|
<button onClick={() => openSurface("notifications")} role="menuitem" type="button">Open notifications</button>
|
|
185
193
|
</div>
|
|
186
194
|
);
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
function NotificationPreview({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
198
|
+
return (
|
|
199
|
+
<article className={`notification-preview ${notification.severity}`}>
|
|
200
|
+
<span aria-hidden="true"><NotificationIcon notification={notification} /></span>
|
|
201
|
+
<div>
|
|
202
|
+
<strong>{notification.title}</strong>
|
|
203
|
+
<small>{notification.category} · {notification.status}</small>
|
|
204
|
+
</div>
|
|
205
|
+
</article>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
189
209
|
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selectedSessionId, status }: {
|
|
190
210
|
conversationMode: ConversationMode;
|
|
191
211
|
selectedLocalRepoIndex: number;
|
|
@@ -268,10 +288,11 @@ function AssistantPanel({ activeSurface, userName }: { activeSurface: SurfaceId;
|
|
|
268
288
|
);
|
|
269
289
|
}
|
|
270
290
|
|
|
271
|
-
function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
291
|
+
function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, status }: {
|
|
272
292
|
activeItem: SurfaceNavItem;
|
|
273
293
|
activeSurface: SurfaceId;
|
|
274
294
|
fileRoot: GuiFileRootId | undefined;
|
|
295
|
+
openSurface: (surface: SurfaceId) => void;
|
|
275
296
|
status: GuiStatusData;
|
|
276
297
|
}) {
|
|
277
298
|
const inventoryConfig = inventorySurfaceConfigs[activeSurface];
|
|
@@ -280,7 +301,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
280
301
|
overview: <OverviewSurface status={status} />,
|
|
281
302
|
help: <HelpSurface />,
|
|
282
303
|
settings: <SettingsSurface status={status} />,
|
|
283
|
-
notifications: <NotificationsSurface />,
|
|
304
|
+
notifications: <NotificationsSurface openSurface={openSurface} status={status} />,
|
|
284
305
|
admin: <AdminSurface />,
|
|
285
306
|
vault: <VaultSurface status={status} />,
|
|
286
307
|
aiSessions: <PlannedSurface label={text.aiSessions} detail={text.aiSessionsIntro} />,
|
|
@@ -391,22 +412,69 @@ function SettingsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
|
391
412
|
);
|
|
392
413
|
}
|
|
393
414
|
|
|
394
|
-
function NotificationsSurface(): ReactElement {
|
|
415
|
+
function NotificationsSurface({ openSurface, status }: { openSurface: (surface: SurfaceId) => void; status: GuiStatusData }): ReactElement {
|
|
416
|
+
const active = status.notifications.filter((notification) => notification.status === "active");
|
|
417
|
+
const resolved = status.notifications.filter((notification) => notification.status === "resolved");
|
|
418
|
+
|
|
395
419
|
return (
|
|
396
|
-
<section className="settings-surface">
|
|
397
|
-
<div className="planned-card">
|
|
420
|
+
<section className="settings-surface notifications-surface">
|
|
421
|
+
<div className="planned-card notifications-hero">
|
|
398
422
|
<h2>Notifications</h2>
|
|
399
|
-
<p>
|
|
423
|
+
<p>OpenCode startup toasts and the GUI notification centre read the same aidevops status cache and local readiness data, so resolved items disappear or downgrade everywhere after the underlying status changes.</p>
|
|
424
|
+
<section aria-label="Notification summary" className="notification-summary-strip">
|
|
425
|
+
<span><strong>{active.length}</strong> active</span>
|
|
426
|
+
<span><strong>{resolved.length}</strong> resolved</span>
|
|
427
|
+
<span><strong>{status.notifications.length}</strong> total</span>
|
|
428
|
+
</section>
|
|
400
429
|
</div>
|
|
401
|
-
<ul className="notification-list">
|
|
402
|
-
<
|
|
403
|
-
<
|
|
404
|
-
<li><strong>Security</strong><span>Vault and credentials posture reminders.</span></li>
|
|
430
|
+
<ul aria-label="aidevops notifications" className="github-notification-list">
|
|
431
|
+
{status.notifications.length === 0 ? <p className="empty-state">No aidevops notifications are currently reported.</p> : null}
|
|
432
|
+
{status.notifications.map((notification) => <NotificationCard key={notification.id} notification={notification} openSurface={openSurface} />)}
|
|
405
433
|
</ul>
|
|
406
434
|
</section>
|
|
407
435
|
);
|
|
408
436
|
}
|
|
409
437
|
|
|
438
|
+
function NotificationCard({ notification, openSurface }: { notification: GuiNotificationSummary; openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
439
|
+
return (
|
|
440
|
+
<li className={`github-notification-card ${notification.severity} ${notification.status}`}>
|
|
441
|
+
<div className="notification-state-icon" aria-hidden="true"><NotificationIcon notification={notification} /></div>
|
|
442
|
+
<div className="notification-card-body">
|
|
443
|
+
<header>
|
|
444
|
+
<div>
|
|
445
|
+
<p className="eyebrow">{notification.category} · {notification.source}</p>
|
|
446
|
+
<h3>{notification.title}</h3>
|
|
447
|
+
</div>
|
|
448
|
+
<span className="notification-status-pill">{notification.status}</span>
|
|
449
|
+
</header>
|
|
450
|
+
<p>{notification.message}</p>
|
|
451
|
+
<small>{notification.source_ref}</small>
|
|
452
|
+
<div className="notification-actions">
|
|
453
|
+
{notification.actions.map((action) => {
|
|
454
|
+
if (action.kind === "surface" && isSurfaceId(action.surface_id)) {
|
|
455
|
+
const surfaceId = action.surface_id;
|
|
456
|
+
return <button className="secondary-action" disabled={!action.enabled} key={action.id} onClick={() => openSurface(surfaceId)} type="button">{action.label}</button>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return <button className="secondary-action" disabled key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</li>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function NotificationIcon({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
468
|
+
if (notification.severity === "error") return <FiAlertTriangle />;
|
|
469
|
+
if (notification.severity === "warning") return <FiTool />;
|
|
470
|
+
if (notification.severity === "success") return <FiCheckCircle />;
|
|
471
|
+
return <FiInfo />;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isSurfaceId(value: string | undefined): value is SurfaceId {
|
|
475
|
+
return surfaceIds.includes(value as SurfaceId);
|
|
476
|
+
}
|
|
477
|
+
|
|
410
478
|
function AdminSurface(): ReactElement {
|
|
411
479
|
return (
|
|
412
480
|
<section className="settings-surface">
|
|
@@ -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,
|
|
@@ -1044,6 +1044,78 @@ code {
|
|
|
1044
1044
|
min-width: 240px;
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
|
+
.notifications-menu {
|
|
1048
|
+
min-width: 320px;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.notifications-menu-heading,
|
|
1052
|
+
.notification-preview {
|
|
1053
|
+
align-items: center;
|
|
1054
|
+
display: flex;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.notifications-menu-heading {
|
|
1058
|
+
justify-content: space-between;
|
|
1059
|
+
padding: 2px 4px 6px;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.notifications-menu-heading span {
|
|
1063
|
+
background: var(--surface-muted);
|
|
1064
|
+
border-radius: 999px;
|
|
1065
|
+
color: var(--text-muted);
|
|
1066
|
+
font-size: 0.76rem;
|
|
1067
|
+
font-weight: 900;
|
|
1068
|
+
padding: 4px 8px;
|
|
1069
|
+
text-transform: uppercase;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.notification-preview {
|
|
1073
|
+
border: 1px solid var(--border);
|
|
1074
|
+
border-left: 4px solid var(--notification-color, var(--accent));
|
|
1075
|
+
border-radius: 12px;
|
|
1076
|
+
gap: 10px;
|
|
1077
|
+
padding: 10px;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.notification-preview > span {
|
|
1081
|
+
color: var(--notification-color, var(--accent));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.notification-preview strong,
|
|
1085
|
+
.notification-preview small {
|
|
1086
|
+
display: block;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.notification-preview small {
|
|
1090
|
+
color: var(--text-muted);
|
|
1091
|
+
font-size: 0.74rem;
|
|
1092
|
+
text-transform: uppercase;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.notification-preview.error,
|
|
1096
|
+
.github-notification-card.error {
|
|
1097
|
+
--notification-color: #cf222e;
|
|
1098
|
+
--notification-bg: rgb(207 34 46 / 10%);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.notification-preview.warning,
|
|
1102
|
+
.github-notification-card.warning {
|
|
1103
|
+
--notification-color: #9a6700;
|
|
1104
|
+
--notification-bg: rgb(191 135 0 / 12%);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.notification-preview.info,
|
|
1108
|
+
.github-notification-card.info {
|
|
1109
|
+
--notification-color: #0969da;
|
|
1110
|
+
--notification-bg: rgb(9 105 218 / 10%);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.notification-preview.success,
|
|
1114
|
+
.github-notification-card.success {
|
|
1115
|
+
--notification-color: #1a7f37;
|
|
1116
|
+
--notification-bg: rgb(26 127 55 / 10%);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1047
1119
|
.popover-menu button,
|
|
1048
1120
|
.popover-menu a {
|
|
1049
1121
|
align-items: center;
|
|
@@ -1440,6 +1512,112 @@ code {
|
|
|
1440
1512
|
color: var(--text-muted);
|
|
1441
1513
|
}
|
|
1442
1514
|
|
|
1515
|
+
.notifications-hero {
|
|
1516
|
+
background:
|
|
1517
|
+
linear-gradient(135deg, var(--notification-bg, color-mix(in srgb, var(--accent) 12%, transparent)), transparent 62%),
|
|
1518
|
+
var(--surface-elevated);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.notification-summary-strip {
|
|
1522
|
+
display: flex;
|
|
1523
|
+
flex-wrap: wrap;
|
|
1524
|
+
gap: 8px;
|
|
1525
|
+
margin-top: 16px;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.notification-summary-strip span,
|
|
1529
|
+
.notification-status-pill {
|
|
1530
|
+
background: var(--surface-muted);
|
|
1531
|
+
border: 1px solid var(--border);
|
|
1532
|
+
border-radius: 999px;
|
|
1533
|
+
color: var(--text-secondary);
|
|
1534
|
+
font-size: 0.8rem;
|
|
1535
|
+
font-weight: 800;
|
|
1536
|
+
padding: 6px 10px;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.notification-summary-strip strong {
|
|
1540
|
+
color: var(--accent);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.github-notification-list {
|
|
1544
|
+
display: grid;
|
|
1545
|
+
gap: 12px;
|
|
1546
|
+
list-style: none;
|
|
1547
|
+
margin: 0;
|
|
1548
|
+
padding: 0;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.github-notification-card {
|
|
1552
|
+
background: var(--surface-elevated);
|
|
1553
|
+
border: 1px solid var(--border);
|
|
1554
|
+
border-left: 5px solid var(--notification-color, var(--accent));
|
|
1555
|
+
border-radius: 18px;
|
|
1556
|
+
box-shadow: var(--glass-panel-shadow);
|
|
1557
|
+
display: grid;
|
|
1558
|
+
gap: 12px;
|
|
1559
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1560
|
+
padding: 16px;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
.github-notification-card.resolved {
|
|
1564
|
+
opacity: 0.78;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.notification-state-icon {
|
|
1568
|
+
align-items: center;
|
|
1569
|
+
background: var(--notification-bg, var(--success-bg));
|
|
1570
|
+
border-radius: 999px;
|
|
1571
|
+
color: var(--notification-color, var(--accent));
|
|
1572
|
+
display: inline-flex;
|
|
1573
|
+
height: 38px;
|
|
1574
|
+
justify-content: center;
|
|
1575
|
+
width: 38px;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.notification-state-icon svg {
|
|
1579
|
+
height: 20px;
|
|
1580
|
+
stroke-width: 2.5;
|
|
1581
|
+
width: 20px;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.notification-card-body {
|
|
1585
|
+
display: grid;
|
|
1586
|
+
gap: 10px;
|
|
1587
|
+
min-width: 0;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.notification-card-body header {
|
|
1591
|
+
align-items: start;
|
|
1592
|
+
display: flex;
|
|
1593
|
+
gap: 12px;
|
|
1594
|
+
justify-content: space-between;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.notification-card-body h3,
|
|
1598
|
+
.notification-card-body p {
|
|
1599
|
+
margin: 0;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.notification-card-body p {
|
|
1603
|
+
color: var(--text-secondary);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
.notification-card-body small {
|
|
1607
|
+
color: var(--text-muted);
|
|
1608
|
+
overflow-wrap: anywhere;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.notification-actions {
|
|
1612
|
+
display: flex;
|
|
1613
|
+
flex-wrap: wrap;
|
|
1614
|
+
gap: 8px;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.notification-actions .secondary-action {
|
|
1618
|
+
padding: 8px 10px;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1443
1621
|
.metric-card span,
|
|
1444
1622
|
.planned-home span,
|
|
1445
1623
|
.data-row.header-row,
|
package/setup.sh
CHANGED
|
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
|
|
|
12
12
|
# AI Assistant Server Access Framework Setup Script
|
|
13
13
|
# Helps developers set up the framework for their infrastructure
|
|
14
14
|
#
|
|
15
|
-
# Version: 3.29.
|
|
15
|
+
# Version: 3.29.6
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|